一、架构设计分析
1.1 为什么选择这套技术栈
电商订单系统具有以下典型特征:高峰期的并发量巨大、事务一致性要求严格、业务流程复杂涉及多个外部服务、以及需要支持实时查询和高性能写入。这些特征使得传统的单体架构在面对业务增长时显得力不从心,而微服务架构虽然解决了扩展性问题,却引入了分布式事务的复杂性。
NestJS 框架基于 TypeScript 和 RxJS 构建,提供了模块化的架构设计和强大的依赖注入系统,非常适合构建企业级复杂业务应用。其内置的 CQRS 模块和事件循环机制为实现 CQRS 模式提供了开箱即用的支持。Redis Stream 作为 Redis 5.0 引入的持久化消息队列,兼具了 Redis 的高性能和消息队列的可靠性,其基于索引的消费机制和消费者组概念使得它非常适合处理订单系统中的异步事件流。相比传统的 Kafka 或 RabbitMQ,Redis Stream 的部署和维护更为简单,且能够与 NestJS 应用共享同一个 Redis 实例,降低了系统复杂度。CQRS 模式将读写操作分离,使得订单的创建和更新(命令端)可以独立于订单查询(查询端)进行优化,这在订单系统中尤为关键——写入时需要严格的事务控制,而查询时则需要灵活的聚合和缓存策略。Saga 模式通过将分布式事务分解为一系列本地事务,并利用补偿机制确保最终一致性,为订单系统中的库存扣减、支付扣款、物流下单等跨服务操作提供了可行的解决方案。
1.2 整体架构图
flowchart TB
subgraph Client["客户端层"]
WebApp["Web Application"]
MobileApp["Mobile Application"]
end
subgraph API["API Gateway / NestJS Application"]
Controller["REST Controller"]
end
subgraph CQRS["CQRS Layer"]
CommandHandler["Command Handler"]
QueryHandler["Query Handler"]
CommandBus["Command Bus"]
QueryBus["Query Bus"]
end
subgraph Saga["Saga Orchestrator"]
OrderSaga["Order Saga"]
Compensations["Compensation Handlers"]
end
subgraph EventBus["Event Bus / Redis Stream"]
EventStore["Event Store"]
StreamProducers["Stream Producers"]
StreamConsumers["Stream Consumers"]
end
subgraph Services["Microservices"]
InventoryService["Inventory Service"]
PaymentService["Payment Service"]
LogisticsService["Logistics Service"]
end
subgraph DataLayer["Data Layer"]
CommandDB["Command Database<br/>(PostgreSQL)"]
QueryDB["Query Database<br/>(Read Replica)"]
RedisCluster["Redis Cluster<br/>(Cache + Stream)"]
end
WebApp --> Controller
MobileApp --> Controller
Controller --> CommandHandler
Controller --> QueryHandler
CommandHandler --> CommandBus
QueryHandler --> QueryBus
CommandBus --> OrderSaga
OrderSaga --> StreamProducers
StreamProducers --> EventStore
EventStore --> StreamConsumers
StreamConsumers --> InventoryService
StreamConsumers --> PaymentService
StreamConsumers --> LogisticsService
InventoryService --> CommandDB
PaymentService --> CommandDB
LogisticsService --> CommandDB
QueryHandler --> QueryDB
QueryHandler --> RedisCluster
OrderSaga -.-> Compensations
Compensations -.-> StreamProducers
1.3 订单系统核心流程
sequenceDiagram
participant Client
participant API as NestJS API
participant Saga as Saga Orchestrator
participant Inventory as Inventory Service
participant Payment as Payment Service
participant Logistics as Logistics Service
participant Redis as Redis Stream
Client->>API: POST /orders (CreateOrderCommand)
API->>Saga: Start Saga
Saga->>Inventory: ReserveInventoryCommand
Inventory-->>Saga: InventoryReserved | InventoryFailed
Saga->>Payment: ProcessPaymentCommand
Payment-->>Saga: PaymentProcessed | PaymentFailed
Saga->>Logistics: CreateShipmentCommand
Logistics-->>Saga: ShipmentCreated | ShipmentFailed
alt Success Path
Saga->>API: OrderCompleted
API-->>Client: Order Response
else Failure Path
Saga->>Inventory: CompensateInventory
Saga->>Payment: RefundPayment
Saga->>API: OrderFailed
API-->>Client: Error Response
end
二、核心概念详解
2.1 CQRS 模式深度解析
CQRS(Command Query Responsibility Segregation,命令查询职责分离)是一种将读取(Query)和写入(Command)操作分离的架构模式。在传统的 CRUD 架构中,同一个数据模型既负责处理业务逻辑和持久化,又负责提供数据查询接口。这种设计在业务简单时足够使用,但随着业务复杂度的增加,会面临诸多挑战:查询逻辑和写入逻辑相互干扰、读写性能难以独立优化、领域模型承担了过多职责。
在订单系统中,CQRS 的价值体现得尤为明显。订单的创建和状态变更是典型的命令操作,需要严格的事务控制和业务规则校验;而订单查询则是典型的读操作,需要支持复杂的查询条件、排序分页、以及可能的缓存策略。将这两类操作分离后,命令端可以专注于业务逻辑的实现,采用写优化的数据结构;查询端则可以专注于查询性能,采用读优化的数据结构,甚至可以使用不同的数据库技术栈(如命令端使用 PostgreSQL 主库保证事务完整性,查询端使用 MongoDB 或只读副本提升查询性能)。
CQRS 模式通常与事件溯源(Event Sourcing)结合使用,但也可以独立存在。在本文的实战方案中,我们采用事件溯源作为状态变更的记录方式,每个订单状态的变化都会产生对应的事件,这些事件被持久化到 Redis Stream 中,既可以作为事件源供查询端消费构建读模型,也可以用于实现 Saga 模式的补偿机制。
2.2 Redis Stream 事件驱动架构
Redis Stream 是 Redis 5.0 引入的数据结构,专门设计用于实现消息队列功能。与传统的 Redis List 和 Pub/Sub 相比,Stream 提供了更强大的功能:支持基于索引的随机访问、支持消费者组实现负载均衡、支持消息持久化和阻塞读取、以及提供了丰富的数据结构命令。
在订单系统中,Redis Stream 主要承担以下职责:作为事件总线,将订单状态变更事件路由到各个下游服务;作为 Saga 的消息传递机制,实现各步骤之间的协调;作为最终一致性的保障,通过重试机制确保消息被可靠处理。Redis Stream 的核心优势在于其极高的写入性能——得益于 Redis 的单线程模型和内存存储特性,Stream 可以轻松支持每秒数万条消息的写入,这对于电商大促期间的订单洪峰是至关重要的。
消费者组(Consumer Group)是 Redis Stream 的另一个核心特性。它允许将消息分配给一组消费者,实现消息的负载均衡和故障转移。当某个消费者实例崩溃时,其未处理的消息会被重新分配给其他消费者,确保消息不会丢失。这一特性使得 Redis Stream 非常适合在容器化部署环境中运行,Pod 的弹性伸缩不会影响消息的处理完整性。
2.3 Saga 模式分布式事务管理
Saga 模式是一种在分布式系统中管理长事务的解决方案,由 Hector Garcia-Molina 和 Kenneth Salem 于 1987 年提出。在传统的分布式事务方案中,两阶段提交(2PC)虽然能够保证强一致性,但其锁等待时间和单点故障问题使其难以在高性能场景中使用。Saga 模式另辟蹊径,将一个长事务分解为一系列本地事务,每个本地事务都有对应的补偿事务。当某个步骤失败时,Saga 会按照相反的顺序执行前面所有步骤的补偿事务,从而实现最终一致性。
Saga 模式有两种主要的实现方式: choreography(编排)和 orchestration(编排器)。Choreography 模式中,各个服务通过事件相互触发,不需要中央协调者;Orchestration 模式则有一个专门的 Saga 编排器来管理整个事务流程,控制各个参与者的调用顺序,并在失败时触发补偿逻辑。本文采用 Orchestration 模式,因为编排器可以提供更清晰的事务边界和更容易理解的业务逻辑。
在订单系统中,Saga 的典型执行流程如下:首先创建订单(状态为 PENDING),然后依次执行库存预占、支付扣款、物流下单等步骤,每个步骤成功后更新订单状态,步骤失败则执行已成功步骤的补偿操作。补偿操作的具体内容取决于业务场景:库存预占的补偿是释放库存、支付扣款的补偿是退款、物流下单的补偿是取消运单。由于补偿操作本身也可能失败,Saga 模式通常需要配合重试机制和幂等性设计一起使用。
三、项目初始化
3.1 项目结构设计
在开始实现之前,我们需要设计一个清晰的项目结构。一个良好的项目结构应该能够反映出架构设计,同时便于团队协作和后续维护。以下是本项目的完整目录结构:
ecommerce-order-system/
├── src/
│ ├── main.ts # 应用入口
│ ├── app.module.ts # 根模块
│ ├── commands/ # CQRS 命令端
│ │ ├── commands/ # 命令定义
│ │ │ ├── create-order.command.ts
│ │ │ ├── cancel-order.command.ts
│ │ │ └── update-order-status.command.ts
│ │ └── handlers/ # 命令处理器
│ │ ├── create-order.handler.ts
│ │ ├── cancel-order.handler.ts
│ │ └── update-order-status.handler.ts
│ ├── queries/ # CQRS 查询端
│ │ ├── queries/ # 查询定义
│ │ │ ├── get-order.query.ts
│ │ │ └── get-orders.query.ts
│ │ └── handlers/ # 查询处理器
│ │ ├── get-order.handler.ts
│ │ └── get-orders.handler.ts
│ ├── saga/ # Saga 编排器
│ │ ├── order.saga.ts
│ │ ├── order-steps.ts
│ │ └── compensations/
│ │ ├── inventory.compensation.ts
│ │ ├── payment.compensation.ts
│ │ └── logistics.compensation.ts
│ ├── events/ # 事件定义
│ │ ├── order.events.ts
│ │ ├── inventory.events.ts
│ │ ├── payment.events.ts
│ │ └── logistics.events.ts
│ ├── integrations/ # 外部服务集成
│ │ ├── inventory/
│ │ │ ├── inventory.service.ts
│ │ │ └── inventory.events-handler.ts
│ │ ├── payment/
│ │ │ ├── payment.service.ts
│ │ │ └── payment.events-handler.ts
│ │ └── logistics/
│ │ ├── logistics.service.ts
│ │ └── logistics.events-handler.ts
│ ├── common/ # 公共模块
│ │ ├── entities/
│ │ │ ├── order.entity.ts
│ │ │ └── order-item.entity.ts
│ │ ├── dto/
│ │ │ ├── create-order.dto.ts
│ │ │ └── order-response.dto.ts
│ │ ├── interfaces/
│ │ │ └── stream.interface.ts
│ │ └── exceptions/
│ │ └── order.exceptions.ts
│ └── config/ # 配置模块
│ ├── redis.config.ts
│ └── database.config.ts
├── test/ # 测试目录
├── package.json
├── tsconfig.json
├── nest-cli.json
└── README.md
四、核心模块实现
4.1 领域实体定义
首先,我们定义订单系统的核心实体。在 CQRS 架构中,命令端的实体更接近领域模型,强调业务不变量的表达;查询端的实体(读模型)则更灵活,可以针对不同的查询场景进行优化。
// src/common/entities/order.entity.ts
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
OneToMany,
} from 'typeorm';
import { OrderItem } from './order-item.entity';
/**
* 订单状态枚举
* 完整的状态流转:PENDING -> INVENTORY_RESERVED -> PAYMENT_PENDING ->
* PAYMENT_COMPLETED -> SHIPPING_PENDING -> SHIPPED -> DELIVERED
* 异常状态:INVENTORY_FAILED / PAYMENT_FAILED / SHIPPING_FAILED / CANCELLED
*/
export enum OrderStatus {
PENDING = 'PENDING', // 订单已创建,等待库存预占
INVENTORY_RESERVED = 'INVENTORY_RESERVED', // 库存预占成功
INVENTORY_FAILED = 'INVENTORY_FAILED', // 库存预占失败
PAYMENT_PENDING = 'PAYMENT_PENDING', // 等待支付
PAYMENT_COMPLETED = 'PAYMENT_COMPLETED', // 支付完成
PAYMENT_FAILED = 'PAYMENT_FAILED', // 支付失败
SHIPPING_PENDING = 'SHIPPING_PENDING', // 等待发货
SHIPPING_PREPARED = 'SHIPPING_PREPARED', // 备货中
SHIPPED = 'SHIPPED', // 已发货
SHIPPING_FAILED = 'SHIPPING_FAILED', // 发货失败
DELIVERED = 'DELIVERED', // 已签收
CANCELLED = 'CANCELLED', // 已取消
REFUND_PENDING = 'REFUND_PENDING', // 退款中
REFUND_COMPLETED = 'REFUND_COMPLETED', // 退款完成
}
/**
* 支付方式枚举
*/
export enum PaymentMethod {
CREDIT_CARD = 'CREDIT_CARD',
DEBIT_CARD = 'DEBIT_CARD',
ALIPAY = 'ALIPAY',
WECHAT_PAY = 'WECHAT_PAY',
BANK_TRANSFER = 'BANK_TRANSFER',
}
/**
* 物流方式枚举
*/
export enum ShippingMethod {
STANDARD = 'STANDARD', // 标准快递
EXPRESS = 'EXPRESS', // 急速快递
PICKUP = 'PICKUP', // 上门自提
}
/**
* 订单实体 - 命令端使用
* 使用 TypeORM 进行持久化
*/
@Entity('orders')
export class Order {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ type: 'varchar', length: 50, unique: true })
orderNumber: string; // 订单编号,格式:ORD-YYYYMMDD-XXXXXX
@Column({ type: 'uuid' })
userId: string; // 用户ID
@Column({ type: 'decimal', precision: 10, scale: 2 })
totalAmount: number; // 订单总金额
@Column({ type: 'decimal', precision: 10, scale: 2 })
freightAmount: number; // 运费
@Column({ type: 'decimal', precision: 10, scale: 2 })
discountAmount: number; // 折扣金额
@Column({ type: 'decimal', precision: 10, scale: 2 })
paymentAmount: number; // 实付金额
@Column({
type: 'enum',
enum: OrderStatus,
default: OrderStatus.PENDING,
})
status: OrderStatus;
@Column({
type: 'enum',
enum: PaymentMethod,
nullable: true,
})
paymentMethod: PaymentMethod;
@Column({ type: 'varchar', length: 100, nullable: true })
paymentTransactionId: string; // 支付交易流水号
@Column({
type: 'enum',
enum: ShippingMethod,
nullable: true,
})
shippingMethod: ShippingMethod;
@Column({ type: 'varchar', length: 50, nullable: true })
trackingNumber: string; // 物流追踪号
@Column({ type: 'text', nullable: true })
shippingAddress: string; // 收货地址(JSON字符串)
@Column({ type: 'text', nullable: true })
remark: string; // 订单备注
@Column({ type: 'int', default: 0 })
retryCount: number; // Saga 重试次数
@Column({ type: 'jsonb', nullable: true })
sagaState: Record<string, any>; // Saga 执行状态快照
@OneToMany(() => OrderItem, (item) => item.order, {
cascade: true,
eager: true,
})
items: OrderItem[];
@CreateDateColumn()
createdAt: Date;
@UpdateDateColumn()
updatedAt: Date;
@Column({ type: 'timestamp', nullable: true })
paidAt: Date; // 支付时间
@Column({ type: 'timestamp', nullable: true })
shippedAt: Date; // 发货时间
@Column({ type: 'timestamp', nullable: true })
deliveredAt: Date; // 签收时间
@Column({ type: 'timestamp', nullable: true })
cancelledAt: Date; // 取消时间
/**
* 领域方法:校验订单是否可以转换为指定状态
*/
canTransitionTo(targetStatus: OrderStatus): boolean {
const validTransitions: Record<OrderStatus, OrderStatus[]> = {
[OrderStatus.PENDING]: [
OrderStatus.INVENTORY_RESERVED,
OrderStatus.INVENTORY_FAILED,
OrderStatus.CANCELLED,
],
[OrderStatus.INVENTORY_RESERVED]: [
OrderStatus.PAYMENT_PENDING,
OrderStatus.CANCELLED,
OrderStatus.INVENTORY_FAILED,
],
[OrderStatus.INVENTORY_FAILED]: [OrderStatus.CANCELLED],
[OrderStatus.PAYMENT_PENDING]: [
OrderStatus.PAYMENT_COMPLETED,
OrderStatus.PAYMENT_FAILED,
OrderStatus.CANCELLED,
],
[OrderStatus.PAYMENT_COMPLETED]: [
OrderStatus.SHIPPING_PENDING,
OrderStatus.REFUND_PENDING,
],
[OrderStatus.PAYMENT_FAILED]: [OrderStatus.CANCELLED],
[OrderStatus.SHIPPING_PENDING]: [
OrderStatus.SHIPPING_PREPARED,
OrderStatus.SHIPPED,
OrderStatus.SHIPPING_FAILED,
],
[OrderStatus.SHIPPING_PREPARED]: [
OrderStatus.SHIPPED,
OrderStatus.SHIPPING_FAILED,
],
[OrderStatus.SHIPPED]: [OrderStatus.DELIVERED],
[OrderStatus.SHIPPING_FAILED]: [OrderStatus.CANCELLED],
[OrderStatus.DELIVERED]: [],
[OrderStatus.CANCELLED]: [],
[OrderStatus.REFUND_PENDING]: [
OrderStatus.REFUND_COMPLETED,
OrderStatus.DELIVERED,
],
[OrderStatus.REFUND_COMPLETED]: [OrderStatus.CANCELLED],
};
return validTransitions[this.status]?.includes(targetStatus) ?? false;
}
/**
* 领域方法:执行状态转换
*/
transitionTo(targetStatus: OrderStatus, metadata?: Record<string, any>): void {
if (!this.canTransitionTo(targetStatus)) {
throw new Error(
`Invalid state transition from ${this.status} to ${targetStatus}`,
);
}
this.status = targetStatus;
// 更新相关时间戳
const now = new Date();
switch (targetStatus) {
case OrderStatus.PAYMENT_COMPLETED:
this.paidAt = now;
break;
case OrderStatus.SHIPPED:
this.shippedAt = now;
break;
case OrderStatus.DELIVERED:
this.deliveredAt = now;
break;
case OrderStatus.CANCELLED:
this.cancelledAt = now;
break;
}
// 记录 Saga 状态快照
this.sagaState = {
...this.sagaState,
lastTransition: {
from: this.status,
to: targetStatus,
timestamp: now,
metadata,
},
};
}
}
// src/common/entities/order-item.entity.ts
import {
Entity,
PrimaryGeneratedColumn,
Column,
ManyToOne,
JoinColumn,
} from 'typeorm';
import { Order } from './order.entity';
/**
* 订单商品项实体
*/
@Entity('order_items')
export class OrderItem {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ type: 'uuid' })
orderId: string;
@Column({ type: 'uuid' })
productId: string;
@Column({ type: 'varchar', length: 255 })
productName: string;
@Column({ type: 'varchar', length: 100 })
productSku: string;
@Column({ type: 'int' })
quantity: number;
@Column({ type: 'decimal', precision: 10, scale: 2 })
unitPrice: number;
@Column({ type: 'decimal', precision: 10, scale: 2 })
subtotal: number;
@Column({ type: 'varchar', length: 50, nullable: true })
inventoryReservationId: string; // 库存预占ID
@ManyToOne(() => Order, (order) => order.items)
@JoinColumn({ name: 'orderId' })
order: Order;
/**
* 计算小计金额
*/
calculateSubtotal(): number {
this.subtotal = this.quantity * Number(this.unitPrice);
return this.subtotal;
}
}
4.2 事件定义
事件驱动架构的核心是事件定义。我们需要为订单系统定义完整的事件体系,包括订单自身事件以及与各外部服务交互的事件。
// src/events/order.events.ts
import { OrderStatus, PaymentMethod, ShippingMethod } from '../common/entities/order.entity';
/**
* 订单相关事件基类
*/
export abstract class OrderEvent {
constructor(
public readonly orderId: string,
public readonly orderNumber: string,
public readonly timestamp: Date = new Date(),
) {}
}
/**
* 订单创建事件
*/
export class OrderCreatedEvent extends OrderEvent {
constructor(
orderId: string,
orderNumber: string,
public readonly userId: string,
public readonly totalAmount: number,
public readonly items: Array<{
productId: string;
productName: string;
productSku: string;
quantity: number;
unitPrice: number;
}>,
public readonly shippingAddress: string,
) {
super(orderId, orderNumber);
}
}
/**
* 库存预占成功事件
*/
export class InventoryReservedEvent extends OrderEvent {
constructor(
orderId: string,
orderNumber: string,
public readonly reservationId: string,
public readonly items: Array<{
productId: string;
quantity: number;
reservationId: string;
}>,
) {
super(orderId, orderNumber);
}
}
/**
* 库存预占失败事件
*/
export class InventoryReservationFailedEvent extends OrderEvent {
constructor(
orderId: string,
orderNumber: string,
public readonly reason: string,
public readonly failedItems: Array<{
productId: string;
requestedQuantity: number;
availableQuantity: number;
}>,
) {
super(orderId, orderNumber);
}
}
/**
* 库存释放事件(补偿操作触发)
*/
export class InventoryReleasedEvent extends OrderEvent {
constructor(
orderId: string,
orderNumber: string,
public readonly reservationId: string,
public readonly items: Array<{
productId: string;
quantity: number;
}>,
) {
super(orderId, orderNumber);
}
}
/**
* 支付发起事件
*/
export class PaymentInitiatedEvent extends OrderEvent {
constructor(
orderId: string,
orderNumber: string,
public readonly userId: string,
public readonly amount: number,
public readonly paymentMethod: PaymentMethod,
) {
super(orderId, orderNumber);
}
}
/**
* 支付成功事件
*/
export class PaymentCompletedEvent extends OrderEvent {
constructor(
orderId: string,
orderNumber: string,
public readonly transactionId: string,
public readonly amount: number,
) {
super(orderId, orderNumber);
}
}
/**
* 支付失败事件
*/
export class PaymentFailedEvent extends OrderEvent {
constructor(
orderId: string,
orderNumber: string,
public readonly reason: string,
public readonly errorCode: string,
) {
super(orderId, orderNumber);
}
}
/**
* 退款成功事件
*/
export class RefundCompletedEvent extends OrderEvent {
constructor(
orderId: string,
orderNumber: string,
public readonly refundId: string,
public readonly refundAmount: number,
) {
super(orderId, orderNumber);
}
}
/**
* 物流下单成功事件
*/
export class ShipmentCreatedEvent extends OrderEvent {
constructor(
orderId: string,
orderNumber: string,
public readonly shipmentId: string,
public readonly trackingNumber: string,
public readonly carrier: string,
public readonly estimatedDelivery: Date,
) {
super(orderId, orderNumber);
}
}
/**
* 物流下单失败事件
*/
export class ShipmentCreationFailedEvent extends OrderEvent {
constructor(
orderId: string,
orderNumber: string,
public readonly reason: string,
) {
super(orderId, orderNumber);
}
}
/**
* 物流取消成功事件
*/
export class ShipmentCancelledEvent extends OrderEvent {
constructor(
orderId: string,
orderNumber: string,
public readonly shipmentId: string,
) {
super(orderId, orderNumber);
}
}
/**
* 订单完成事件(所有流程成功结束)
*/
export class OrderCompletedEvent extends OrderEvent {
constructor(
orderId: string,
orderNumber: string,
public readonly completedAt: Date = new Date(),
) {
super(orderId, orderNumber);
}
}
/**
* 订单取消事件
*/
export class OrderCancelledEvent extends OrderEvent {
constructor(
orderId: string,
orderNumber: string,
public readonly reason: string,
public readonly cancelledBy: 'USER' | 'SYSTEM',
) {
super(orderId, orderNumber);
}
}
/**
* Saga 补偿开始事件
*/
export class SagaCompensationStartedEvent extends OrderEvent {
constructor(
orderId: string,
orderNumber: string,
public readonly failedStep: string,
public readonly reason: string,
) {
super(orderId, orderNumber);
}
}
/**
* Saga 补偿完成事件
*/
export class SagaCompensationCompletedEvent extends OrderEvent {
constructor(
orderId: string,
orderNumber: string,
public readonly compensatedSteps: string[],
) {
super(orderId, orderNumber);
}
}
4.3 CQRS 命令端实现
命令端是订单写入的入口,负责处理所有修改订单状态的请求。在 CQRS 模式中,每个命令都会触发相应的事件,这些事件被发送到 Redis Stream 供 Saga 和其他消费者处理。
// src/commands/commands/create-order.command.ts
import { ICommand } from '@nestjs/cqrs';
import { PaymentMethod, ShippingMethod } from '../../common/entities/order.entity';
/**
* 创建订单命令
* 这是订单系统的核心命令之一
*/
export class CreateOrderCommand implements ICommand {
constructor(
public readonly userId: string,
public readonly items: Array<{
productId: string;
productName: string;
productSku: string;
quantity: number;
unitPrice: number;
}>,
public readonly shippingAddress: {
receiverName: string;
receiverPhone: string;
province: string;
city: string;
district: string;
detailAddress: string;
},
public readonly paymentMethod: PaymentMethod,
public readonly shippingMethod: ShippingMethod,
public readonly remark?: string,
public readonly couponCodes?: string[],
) {}
}
// src/commands/commands/cancel-order.command.ts
import { ICommand } from '@nestjs/cqrs';
/**
* 取消订单命令
*/
export class CancelOrderCommand implements ICommand {
constructor(
public readonly orderId: string,
public readonly userId: string,
public readonly reason: string,
public readonly forceCancel: boolean = false, // 是否强制取消(跳过 Saga 补偿)
) {}
}
// src/commands/commands/update-order-status.command.ts
import { ICommand } from '@nestjs/cqrs';
import { OrderStatus } from '../../common/entities/order.entity';
/**
* 更新订单状态命令
* 一般用于外部回调触发状态更新
*/
export class UpdateOrderStatusCommand implements ICommand {
constructor(
public readonly orderId: string,
public readonly targetStatus: OrderStatus,
public readonly metadata?: Record<string, any>,
public readonly operatorId?: string,
) {}
}
// src/commands/handlers/create-order.handler.ts
import { CommandHandler, ICommandHandler, EventBus } from '@nestjs/cqrs';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { CreateOrderCommand } from '../commands/create-order.command';
import { Order, OrderStatus } from '../../common/entities/order.entity';
import { OrderItem } from '../../common/entities/order-item.entity';
import { OrderCreatedEvent } from '../../events/order.events';
import { v4 as uuidv4 } from 'uuid';
/**
* 创建订单命令处理器
* 负责验证订单数据、创建订单实体、发布订单创建事件
*/
@CommandHandler(CreateOrderCommand)
export class CreateOrderHandler implements ICommandHandler<CreateOrderCommand> {
constructor(
@InjectRepository(Order)
private readonly orderRepository: Repository<Order>,
@InjectRepository(OrderItem)
private readonly orderItemRepository: Repository<OrderItem>,
private readonly eventBus: EventBus,
) {}
async execute(command: CreateOrderCommand): Promise<Order> {
// 1. 生成订单编号
const orderNumber = this.generateOrderNumber();
// 2. 计算订单金额
const { subtotal, freightAmount, discountAmount, paymentAmount } =
this.calculateOrderAmount(command.items, command.shippingMethod);
// 3. 创建订单实体
const order = this.orderRepository.create({
id: uuidv4(),
orderNumber,
userId: command.userId,
totalAmount: subtotal,
freightAmount,
discountAmount,
paymentAmount,
status: OrderStatus.PENDING,
paymentMethod: command.paymentMethod,
shippingMethod: command.shippingMethod,
shippingAddress: JSON.stringify(command.shippingAddress),
remark: command.remark,
});
// 4. 创建订单商品项
const orderItems = command.items.map((item) =>
this.orderItemRepository.create({
id: uuidv4(),
orderId: order.id,
productId: item.productId,
productName: item.productName,
productSku: item.productSku,
quantity: item.quantity,
unitPrice: item.unitPrice,
subtotal: item.quantity * item.unitPrice,
}),
);
order.items = orderItems;
// 5. 保存订单
const savedOrder = await this.orderRepository.save(order);
await this.orderItemRepository.save(orderItems);
// 6. 发布订单创建事件(触发 Saga)
const orderCreatedEvent = new OrderCreatedEvent(
savedOrder.id,
savedOrder.orderNumber,
savedOrder.userId,
savedOrder.paymentAmount,
command.items.map((item) => ({
productId: item.productId,
productName: item.productName,
productSku: item.productSku,
quantity: item.quantity,
unitPrice: item.unitPrice,
})),
command.shippingAddress as any,
);
this.eventBus.publish(orderCreatedEvent);
return savedOrder;
}
/**
* 生成唯一订单编号
* 格式:ORD-YYYYMMDD-XXXXXX(6位随机数)
*/
private generateOrderNumber(): string {
const date = new Date();
const dateStr = date.toISOString().slice(0, 10).replace(/-/g, '');
const random = Math.random().toString(36).substring(2, 8).toUpperCase();
return `ORD-${dateStr}-${random}`;
}
/**
* 计算订单金额
*/
private calculateOrderAmount(
items: Array<{ quantity: number; unitPrice: number }>,
shippingMethod: ShippingMethod,
): {
subtotal: number;
freightAmount: number;
discountAmount: number;
paymentAmount: number;
} {
const subtotal = items.reduce(
(sum, item) => sum + item.quantity * item.unitPrice,
0,
);
// 计算运费:标准快递固定10元,急速快递20元,自提免运费
let freightAmount = 10;
if (shippingMethod === ShippingMethod.EXPRESS) {
freightAmount = 20;
} else if (shippingMethod === ShippingMethod.PICKUP) {
freightAmount = 0;
}
// 模拟折扣:满100减10
let discountAmount = 0;
if (subtotal >= 100) {
discountAmount = Math.floor(subtotal / 100) * 10;
}
const paymentAmount = subtotal + freightAmount - discountAmount;
return { subtotal, freightAmount, discountAmount, paymentAmount };
}
}
// src/commands/handlers/cancel-order.handler.ts
import {
CommandHandler,
ICommandHandler,
EventBus,
} from '@nestjs/cqrs';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { CancelOrderCommand } from '../commands/cancel-order.command';
import { Order, OrderStatus } from '../../common/entities/order.entity';
import { OrderCancelledEvent } from '../../events/order.events';
/**
* 取消订单命令处理器
*/
@CommandHandler(CancelOrderCommand)
export class CancelOrderHandler implements ICommandHandler<CancelOrderCommand> {
constructor(
@InjectRepository(Order)
private readonly orderRepository: Repository<Order>,
private readonly eventBus: EventBus,
) {}
async execute(command: CancelOrderCommand): Promise<Order> {
const order = await this.orderRepository.findOne({
where: { id: command.orderId, userId: command.userId },
});
if (!order) {
throw new Error(`Order ${command.orderId} not found`);
}
// 校验订单是否可以取消
const cancellableStatuses = [
OrderStatus.PENDING,
OrderStatus.INVENTORY_RESERVED,
OrderStatus.PAYMENT_PENDING,
OrderStatus.INVENTORY_FAILED,
OrderStatus.PAYMENT_FAILED,
];
if (!cancellableStatuses.includes(order.status)) {
throw new Error(
`Order ${command.orderId} cannot be cancelled in status ${order.status}`,
);
}
// 更新订单状态
order.status = OrderStatus.CANCELLED;
const savedOrder = await this.orderRepository.save(order);
// 发布取消事件(触发 Saga 补偿)
const cancelEvent = new OrderCancelledEvent(
savedOrder.id,
savedOrder.orderNumber,
command.reason,
'USER',
);
this.eventBus.publish(cancelEvent);
return savedOrder;
}
}
4.4 CQRS 查询端实现
查询端负责处理所有订单查询请求。在 CQRS 模式中,查询端可以使用专门针对读场景优化的数据模型,如使用 MongoDB 支持灵活的聚合查询,或者使用 PostgreSQL 的只读副本减少主库压力。查询端的数据来源可以是直接查询数据库,也可以是从 Redis 缓存中获取。
// src/queries/queries/get-order.query.ts
import { IQuery } from '@nestjs/cqrs';
/**
* 获取单个订单查询
*/
export class GetOrderQuery implements IQuery {
constructor(
public readonly orderId: string,
public readonly userId?: string,
) {}
}
// src/queries/queries/get-orders.query.ts
import { IQuery } from '@nestjs/cqrs';
import { OrderStatus } from '../../common/entities/order.entity';
/**
* 获取订单列表查询
*/
export class GetOrdersQuery implements IQuery {
constructor(
public readonly userId: string,
public readonly options: {
status?: OrderStatus;
page?: number;
pageSize?: number;
startDate?: Date;
endDate?: Date;
sortBy?: 'createdAt' | 'updatedAt' | 'totalAmount';
sortOrder?: 'ASC' | 'DESC';
},
) {}
}
// src/queries/handlers/get-order.handler.ts
import { QueryHandler, IQueryHandler } from '@nestjs/cqrs';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { GetOrderQuery } from '../queries/get-order.query';
import { Order, OrderStatus } from '../../common/entities/order.entity';
import { Redis } from 'ioredis';
/**
* 获取单个订单查询处理器
* 实现多级缓存策略:Redis -> 数据库
*/
@QueryHandler(GetOrderQuery)
export class GetOrderHandler implements IQueryHandler<GetOrderQuery> {
private readonly CACHE_TTL = 300; // 5分钟缓存
constructor(
@InjectRepository(Order)
private readonly orderRepository: Repository<Order>,
private readonly redisClient: Redis,
) {}
async execute(query: GetOrderQuery): Promise<any> {
const cacheKey = `order:${query.orderId}`;
// 1. 尝试从 Redis 缓存获取
const cached = await this.redisClient.get(cacheKey);
if (cached) {
const order = JSON.parse(cached);
// 如果指定了 userId,校验所有权
if (query.userId && order.userId !== query.userId) {
return null;
}
return order;
}
// 2. 从数据库查询
const queryBuilder = this.orderRepository
.createQueryBuilder('order')
.leftJoinAndSelect('order.items', 'items');
if (query.userId) {
queryBuilder.where('order.userId = :userId', { userId: query.userId });
}
const order = await queryBuilder
.andWhere('order.id = :orderId', { orderId: query.orderId })
.getOne();
if (!order) {
return null;
}
// 3. 格式化返回数据
const response = this.formatOrderResponse(order);
// 4. 写入缓存
await this.redisClient.setex(
cacheKey,
this.CACHE_TTL,
JSON.stringify(response),
);
return response;
}
/**
* 格式化订单响应数据
* 查询端可以根据客户端需求定制返回格式
*/
private formatOrderResponse(order: Order): any {
return {
id: order.id,
orderNumber: order.orderNumber,
userId: order.userId,
status: order.status,
statusText: this.getStatusText(order.status),
items: order.items.map((item) => ({
productId: item.productId,
productName: item.productName,
productSku: item.productSku,
quantity: item.quantity,
unitPrice: Number(item.unitPrice),
subtotal: Number(item.subtotal),
})),
amount: {
subtotal: Number(order.totalAmount),
freight: Number(order.freightAmount),
discount: Number(order.discountAmount),
payment: Number(order.paymentAmount),
},
payment: {
method: order.paymentMethod,
transactionId: order.paymentTransactionId,
},
shipping: {
method: order.shippingMethod,
trackingNumber: order.trackingNumber,
address: order.shippingAddress ? JSON.parse(order.shippingAddress) : null,
},
timestamps: {
created: order.createdAt,
updated: order.updatedAt,
paid: order.paidAt,
shipped: order.shippedAt,
delivered: order.deliveredAt,
cancelled: order.cancelledAt,
},
remark: order.remark,
};
}
/**
* 获取订单状态的中文描述
*/
private getStatusText(status: OrderStatus): string {
const statusMap: Record<OrderStatus, string> = {
[OrderStatus.PENDING]: '待处理',
[OrderStatus.INVENTORY_RESERVED]: '库存已预留',
[OrderStatus.INVENTORY_FAILED]: '库存预占失败',
[OrderStatus.PAYMENT_PENDING]: '等待支付',
[OrderStatus.PAYMENT_COMPLETED]: '支付成功',
[OrderStatus.PAYMENT_FAILED]: '支付失败',
[OrderStatus.SHIPPING_PENDING]: '等待发货',
[OrderStatus.SHIPPING_PREPARED]: '备货中',
[OrderStatus.SHIPPED]: '已发货',
[OrderStatus.SHIPPING_FAILED]: '发货失败',
[OrderStatus.DELIVERED]: '已签收',
[OrderStatus.CANCELLED]: '已取消',
[OrderStatus.REFUND_PENDING]: '退款中',
[OrderStatus.REFUND_COMPLETED]: '退款完成',
};
return statusMap[status];
}
}
// src/queries/handlers/get-orders.handler.ts
import { QueryHandler, IQueryHandler } from '@nestjs/cqrs';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { GetOrdersQuery } from '../queries/get-orders.query';
import { Order, OrderStatus } from '../../common/entities/order.entity';
/**
* 获取订单列表查询处理器
* 支持分页、筛选、排序
*/
@QueryHandler(GetOrdersQuery)
export class GetOrdersHandler implements IQueryHandler<GetOrdersQuery> {
constructor(
@InjectRepository(Order)
private readonly orderRepository: Repository<Order>,
) {}
async execute(query: GetOrdersQuery): Promise<{
orders: any[];
total: number;
page: number;
pageSize: number;
totalPages: number;
}> {
const {
userId,
options: {
status,
page = 1,
pageSize = 10,
startDate,
endDate,
sortBy = 'createdAt',
sortOrder = 'DESC',
},
} = query;
const queryBuilder = this.orderRepository
.createQueryBuilder('order')
.leftJoinAndSelect('order.items', 'items')
.where('order.userId = :userId', { userId });
// 应用筛选条件
if (status) {
queryBuilder.andWhere('order.status = :status', { status });
}
if (startDate) {
queryBuilder.andWhere('order.createdAt >= :startDate', { startDate });
}
if (endDate) {
queryBuilder.andWhere('order.createdAt <= :endDate', { endDate });
}
// 计算分页
const skip = (page - 1) * pageSize;
queryBuilder.skip(skip).take(pageSize);
// 应用排序
const sortField = `order.${sortBy}`;
queryBuilder.orderBy(sortField, sortOrder);
// 执行查询
const [orders, total] = await queryBuilder.getManyAndCount();
return {
orders: orders.map((order) => ({
id: order.id,
orderNumber: order.orderNumber,
status: order.status,
statusText: this.getStatusText(order.status),
itemCount: order.items.length,
totalAmount: Number(order.paymentAmount),
createdAt: order.createdAt,
updatedAt: order.updatedAt,
})),
total,
page,
pageSize,
totalPages: Math.ceil(total / pageSize),
};
}
private getStatusText(status: OrderStatus): string {
const statusMap: Record<OrderStatus, string> = {
[OrderStatus.PENDING]: '待处理',
[OrderStatus.INVENTORY_RESERVED]: '库存已预留',
[OrderStatus.INVENTORY_FAILED]: '库存预占失败',
[OrderStatus.PAYMENT_PENDING]: '等待支付',
[OrderStatus.PAYMENT_COMPLETED]: '支付成功',
[OrderStatus.PAYMENT_FAILED]: '支付失败',
[OrderStatus.SHIPPING_PENDING]: '等待发货',
[OrderStatus.SHIPPING_PREPARED]: '备货中',
[OrderStatus.SHIPPED]: '已发货',
[OrderStatus.SHIPPING_FAILED]: '发货失败',
[OrderStatus.DELIVERED]: '已签收',
[OrderStatus.CANCELLED]: '已取消',
[OrderStatus.REFUND_PENDING]: '退款中',
[OrderStatus.REFUND_COMPLETED]: '退款完成',
};
return statusMap[status];
}
}
五、Redis Stream 配置与事件处理
5.1 Redis Stream 服务配置
Redis Stream 是整个系统的核心消息中枢。我们需要创建一个专门的模块来管理 Redis 连接和 Stream 的读写操作。
// src/common/interfaces/stream.interface.ts
import { Consumer, StreamEntry } from 'ioredis';
/**
* Redis Stream 通道名称定义
*/
export const STREAM_CHANNELS = {
ORDER_EVENTS: 'stream:order:events',
INVENTORY_EVENTS: 'stream:inventory:events',
PAYMENT_EVENTS: 'stream:payment:events',
LOGISTICS_EVENTS: 'stream:logistics:events',
SAGA_COMMANDS: 'stream:saga:commands',
SAGA_COMPENSATIONS: 'stream:saga:compensations',
} as const;
/**
* 消费者组配置
*/
export const CONSUMER_GROUPS = {
ORDER_SAGA: 'saga-order',
INVENTORY_SERVICE: 'service-inventory',
PAYMENT_SERVICE: 'service-payment',
LOGISTICS_SERVICE: 'service-logistics',
NOTIFICATION_SERVICE: 'service-notification',
} as const;
/**
* Stream 消息结构接口
*/
export interface IStreamMessage<T = any> {
id: string;
channel: string;
data: T;
timestamp: Date;
retryCount: number;
}
/**
* 事件处理结果
*/
export interface IEventHandlerResult {
success: boolean;
error?: string;
shouldRetry?: boolean;
}
// src/config/redis.config.ts
import { Module, Global } from '@nestjs/common';
import { Redis } from 'ioredis';
import { STREAM_CHANNELS, CONSUMER_GROUPS } from '../common/interfaces/stream.interface';
/**
* Redis 配置常量
*/
export const REDIS_CONFIG = {
host: process.env.REDIS_HOST || 'localhost',
port: parseInt(process.env.REDIS_PORT || '6379', 10),
password: process.env.REDIS_PASSWORD || undefined,
db: parseInt(process.env.REDIS_DB || '0', 10),
keyPrefix: 'ecommerce:',
} as const;
/**
* 全局 Redis 提供者
*/
export const REDIS_CLIENT = 'REDIS_CLIENT';
/**
* Redis 模块
* 提供 Redis 连接实例和 Stream 操作接口
*/
@Global()
@Module({
providers: [
{
provide: REDIS_CLIENT,
useFactory: async () => {
const redis = new Redis({
host: REDIS_CONFIG.host,
port: REDIS_CONFIG.port,
password: REDIS_CONFIG.password,
db: REDIS_CONFIG.db,
keyPrefix: REDIS_CONFIG.keyPrefix,
retryStrategy: (times) => {
const delay = Math.min(times * 50, 2000);
return delay;
},
maxRetriesPerRequest: 3,
});
redis.on('connect', () => {
console.log('[Redis] Connected successfully');
});
redis.on('error', (err) => {
console.error('[Redis] Connection error:', err);
});
// 初始化 Stream 和消费者组
await initializeStreams(redis);
return redis;
},
},
],
exports: [REDIS_CLIENT],
})
export class RedisModule {}
/**
* 初始化 Redis Stream 和消费者组
*/
async function initializeStreams(redis: Redis): Promise<void> {
const streams = Object.values(STREAM_CHANNELS);
const groups = Object.values(CONSUMER_GROUPS);
for (const stream of streams) {
try {
// 创建 Stream(如果不存在)
// XGROUP CREATE stream:order:events saga-order 0 MKSTREAM
await redis.xgroup(
'CREATE',
stream,
groups[0], // 使用默认的第一个消费者组
'0',
'MKSTREAM',
);
console.log(`[Redis] Stream ${stream} and consumer group created`);
} catch (error: any) {
if (error.message.includes('BUSYGROUP')) {
// 消费者组已存在,忽略
console.log(`[Redis] Consumer group for ${stream} already exists`);
} else {
console.error(`[Redis] Failed to initialize stream ${stream}:`, error);
}
}
}
}
5.2 事件发布服务
事件发布是连接 Saga 和外部服务的桥梁。所有事件都通过统一的事件发布服务发送到 Redis Stream,确保消息的可靠性和可追溯性。
// src/events/stream-publisher.service.ts
import { Injectable, Inject, OnModuleInit } from '@nestjs/common';
import { Redis } from 'ioredis';
import { REDIS_CLIENT } from '../config/redis.config';
import { STREAM_CHANNELS } from '../common/interfaces/stream.interface';
/**
* Stream 事件发布服务
* 负责将事件发布到 Redis Stream
*/
@Injectable()
export class StreamPublisherService implements OnModuleInit {
constructor(@Inject(REDIS_CLIENT) private readonly redis: Redis) {}
onModuleInit() {
console.log('[StreamPublisher] Initialized');
}
/**
* 发布事件到指定 Stream
* @param channel Stream 通道名称
* @param event 事件数据
* @param eventType 事件类型(用于消息标识)
*/
async publish<T>(
channel: string,
event: T,
eventType: string,
): Promise<string> {
const messageId = await this.redis.xadd(
channel,
'*',
'eventType',
eventType,
'data',
JSON.stringify(event),
'timestamp',
new Date().toISOString(),
);
console.log(
`[StreamPublisher] Published ${eventType} to ${channel}, messageId: ${messageId}`,
);
return messageId;
}
/**
* 发布订单相关事件
*/
async publishOrderEvent<T>(event: T, eventType: string): Promise<string> {
return this.publish(STREAM_CHANNELS.ORDER_EVENTS, event, eventType);
}
/**
* 发布库存相关事件
*/
async publishInventoryEvent<T>(event: T, eventType: string): Promise<string> {
return this.publish(STREAM_CHANNELS.INVENTORY_EVENTS, event, eventType);
}
/**
* 发布支付相关事件
*/
async publishPaymentEvent<T>(event: T, eventType: string): Promise<string> {
return this.publish(STREAM_CHANNELS.PAYMENT_EVENTS, event, eventType);
}
/**
* 发布物流相关事件
*/
async publishLogisticsEvent<T>(event: T, eventType: string): Promise<string> {
return this.publish(STREAM_CHANNELS.LOGISTICS_EVENTS, event, eventType);
}
/**
* 发布 Saga 命令
*/
async publishSagaCommand<T>(event: T, commandType: string): Promise<string> {
return this.publish(STREAM_CHANNELS.SAGA_COMMANDS, event, commandType);
}
/**
* 发布 Saga 补偿命令
*/
async publishSagaCompensation<T>(
event: T,
compensationType: string,
): Promise<string> {
return this.publish(STREAM_CHANNELS.SAGA_COMPENSATIONS, event, compensationType);
}
}
5.3 事件消费服务
事件消费服务负责从 Redis Stream 读取消息并分发给相应的处理器。它支持消费者组模式,可以实现负载均衡和故障转移。
// src/events/stream-consumer.service.ts
import { Injectable, Inject, OnModuleDestroy } from '@nestjs/common';
import { Redis } from 'ioredis';
import { REDIS_CLIENT } from '../config/redis.config';
import {
STREAM_CHANNELS,
CONSUMER_GROUPS,
IStreamMessage,
} from '../common/interfaces/stream.interface';
/**
* Stream 事件消费服务
* 实现了基于消费者组的消息消费模式
*/
@Injectable()
export class StreamConsumerService implements OnModuleDestroy {
private isRunning = false;
private consumers: Map<string, NodeJS.Timeout> = new Map();
constructor(@Inject(REDIS_CLIENT) private readonly redis: Redis) {}
onModuleDestroy() {
this.stopAllConsumers();
}
/**
* 开始消费指定 Stream
* @param channel Stream 通道
* @param consumerGroup 消费者组
* @param handler 消息处理函数
* @param options 消费选项
*/
async startConsuming(
channel: string,
consumerGroup: string,
handler: (message: IStreamMessage) => Promise<void>,
options: {
batchSize?: number;
blockMs?: number;
consumerName?: string;
} = {},
): Promise<void> {
const {
batchSize = 10,
blockMs = 5000,
consumerName = `consumer-${process.pid}`,
} = options;
this.isRunning = true;
// 启动消费循环
const consumerId = `${channel}:${consumerGroup}:${consumerName}`;
console.log(`[StreamConsumer] Starting consumer ${consumerId}`);
const loop = async () => {
while (this.isRunning) {
try {
// 使用 XREADGROUP 读取消息
// 语法:XREADGROUP GROUP <group> <consumer> COUNT <count> BLOCK <ms> STREAMS <stream> ...
const result = await this.redis.xreadgroup(
'GROUP',
consumerGroup,
consumerName,
'COUNT',
batchSize.toString(),
'BLOCK',
blockMs.toString(),
'STREAMS',
channel,
'>', // '>' 表示读取新消息,'0' 表示读取待确认消息
);
if (!result) {
continue;
}
// 处理每条消息
for (const [streamName, messages] of result) {
for (const [messageId, fields] of messages) {
const data = this.parseMessageFields(fields);
const streamMessage: IStreamMessage = {
id: messageId,
channel: streamName,
data: data.data,
timestamp: new Date(data.timestamp),
retryCount: 0,
};
try {
await handler(streamMessage);
// 确认消息处理成功
await this.redis.xack(channel, consumerGroup, messageId);
console.log(
`[StreamConsumer] Processed message ${messageId} from ${channel}`,
);
} catch (error) {
console.error(
`[StreamConsumer] Error processing message ${messageId}:`,
error,
);
// 可以在这里实现重试逻辑
// 失败消息可以发送到死信队列
}
}
}
} catch (error) {
console.error(`[StreamConsumer] Error in consume loop:`, error);
// 等待一段时间后重试
await new Promise((resolve) => setTimeout(resolve, 1000));
}
}
};
this.consumers.set(consumerId, setImmediate(loop));
}
/**
* 解析消息字段
*/
private parseMessageFields(
fields: string[],
): Record<string, any> {
const result: Record<string, any> = {};
for (let i = 0; i < fields.length; i += 2) {
const key = fields[i];
const value = fields[i + 1];
if (key === 'data') {
try {
result[key] = JSON.parse(value);
} catch {
result[key] = value;
}
} else {
result[key] = value;
}
}
return result;
}
/**
* 停止所有消费者
*/
private stopAllConsumers(): void {
this.isRunning = false;
for (const [id, timeout] of this.consumers) {
clearImmediate(timeout);
console.log(`[StreamConsumer] Stopped consumer ${id}`);
}
this.consumers.clear();
}
/**
* 获取 Stream 信息
*/
async getStreamInfo(channel: string): Promise<{
length: number;
firstEntry: string;
lastEntry: string;
}> {
const info = await this.redis.xinfo('STREAM', channel.replace('ecommerce:', ''));
// 解析 XINFO STREAM 输出
const infoMap: Record<string, any> = {};
for (let i = 0; i < info.length; i += 2) {
infoMap[info[i]] = info[i + 1];
}
return {
length: infoMap['length'] || 0,
firstEntry: infoMap['first-entry']?.[0] || 'N/A',
lastEntry: infoMap['last-entry']?.[0] || 'N/A',
};
}
/**
* 获取消费者组信息
*/
async getConsumerGroupInfo(
channel: string,
group: string,
): Promise<{
name: string;
consumers: number;
pending: number;
lastDeliveredId: string;
}> {
const info = await this.redis.xinfo('GROUPS', channel.replace('ecommerce:', ''));
const groupInfo = info.find((g: any) => g[1] === group);
if (!groupInfo) {
return {
name: group,
consumers: 0,
pending: 0,
lastDeliveredId: '0-0',
};
}
return {
name: groupInfo[1],
consumers: groupInfo[2],
pending: groupInfo[3],
lastDeliveredId: groupInfo[4],
};
}
}
六、Saga 编排器实现
6.1 Saga 步骤定义
Saga 编排器的核心是将订单处理流程分解为一系列步骤,每个步骤对应一个具体的业务操作及其补偿操作。
// src/saga/order-steps.ts
import { OrderStatus } from '../common/entities/order.entity';
/**
* Saga 步骤接口
*/
export interface ISagaStep {
name: string;
execute: () => Promise<SagaStepResult>;
compensate: () => Promise<void>;
getTimeout?: () => number;
}
/**
* Saga 步骤执行结果
*/
export interface SagaStepResult {
success: boolean;
data?: any;
error?: string;
errorCode?: string;
}
/**
* 预定义 Saga 步骤名称
*/
export const SAGA_STEPS = {
RESERVE_INVENTORY: 'reserve_inventory',
PROCESS_PAYMENT: 'process_payment',
CREATE_SHIPMENT: 'create_shipment',
} as const;
/**
* 步骤执行顺序
*/
export const STEP_EXECUTION_ORDER = [
SAGA_STEPS.RESERVE_INVENTORY,
SAGA_STEPS.PROCESS_PAYMENT,
SAGA_STEPS.CREATE_SHIPMENT,
] as const;
/**
* 步骤到状态映射
* 定义每个步骤成功后将订单状态更新为什么
*/
export const STEP_TO_STATUS_MAP: Record<string, OrderStatus> = {
[SAGA_STEPS.RESERVE_INVENTORY]: OrderStatus.INVENTORY_RESERVED,
[SAGA_STEPS.PROCESS_PAYMENT]: OrderStatus.PAYMENT_COMPLETED,
[SAGA_STEPS.CREATE_SHIPMENT]: OrderStatus.SHIPPED,
};
6.2 Saga 编排器核心实现
Saga 编排器负责协调整个订单处理流程,包括步骤执行、状态管理、补偿触发和重试逻辑。
// src/saga/order.saga.ts
import { Injectable, Inject, OnModuleInit } from '@nestjs/common';
import { EventBus } from '@nestjs/cqrs';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Order, OrderStatus } from '../common/entities/order.entity';
import { StreamPublisherService } from '../events/stream-publisher.service';
import { StreamConsumerService } from '../events/stream-consumer.service';
import {
STREAM_CHANNELS,
CONSUMER_GROUPS,
IStreamMessage,
} from '../common/interfaces/stream.interface';
import {
ISagaStep,
SagaStepResult,
SAGA_STEPS,
STEP_EXECUTION_ORDER,
STEP_TO_STATUS_MAP,
} from './order-steps';
import {
OrderCreatedEvent,
InventoryReservedEvent,
InventoryReservationFailedEvent,
PaymentCompletedEvent,
PaymentFailedEvent,
ShipmentCreatedEvent,
ShipmentCreationFailedEvent,
OrderCompletedEvent,
SagaCompensationStartedEvent,
} from '../events/order.events';
import { InventoryCompensation } from './compensations/inventory.compensation';
import { PaymentCompensation } from './compensations/payment.compensation';
import { LogisticsCompensation } from './compensations/logistics.compensation';
/**
* Saga 状态追踪
*/
interface SagaState {
orderId: string;
orderNumber: string;
currentStep: number;
completedSteps: string[];
stepResults: Record<string, SagaStepResult>;
compensationInProgress: boolean;
startedAt: Date;
updatedAt: Date;
}
/**
* Saga 编排器
* 核心职责:
* 1. 管理订单处理的生命周期
* 2. 按顺序执行各个步骤
* 3. 处理步骤失败并触发补偿
* 4. 维护 Saga 状态快照
*/
@Injectable()
export class OrderSaga implements OnModuleInit {
// Saga 超时配置(毫秒)
private readonly STEP_TIMEOUT = 30000;
// 最大重试次数
private readonly MAX_RETRIES = 3;
// 内存中的 Saga 状态缓存
private sagaStates: Map<string, SagaState> = new Map();
constructor(
@InjectRepository(Order)
private readonly orderRepository: Repository<Order>,
private readonly eventBus: EventBus,
private readonly streamPublisher: StreamPublisherService,
private readonly streamConsumer: StreamConsumerService,
private readonly inventoryCompensation: InventoryCompensation,
private readonly paymentCompensation: PaymentCompensation,
private readonly logisticsCompensation: LogisticsCompensation,
) {}
onModuleInit() {
// 启动 Saga 命令消费者
this.startSagaConsumer();
}
/**
* 启动 Saga 命令消费者
* 监听 Redis Stream 中的 Saga 命令
*/
private async startSagaConsumer(): Promise<void> {
await this.streamConsumer.startConsuming(
STREAM_CHANNELS.ORDER_EVENTS,
CONSUMER_GROUPS.ORDER_SAGA,
async (message: IStreamMessage) => {
const { eventType, data } = message.data;
switch (eventType) {
case 'OrderCreated':
await this.handleOrderCreated(data);
break;
case 'OrderCancelled':
await this.handleOrderCancelled(data);
break;
// 其他事件处理...
}
},
{
consumerName: `saga-orchestrator-${process.pid}`,
batchSize: 5,
blockMs: 5000,
},
);
}
/**
* 处理订单创建事件
* 启动新的 Saga 实例
*/
async handleOrderCreated(event: OrderCreatedEvent): Promise<void> {
console.log(`[OrderSaga] Starting saga for order ${event.orderNumber}`);
// 初始化 Saga 状态
const sagaState: SagaState = {
orderId: event.orderId,
orderNumber: event.orderNumber,
currentStep: 0,
completedSteps: [],
stepResults: {},
compensationInProgress: false,
startedAt: new Date(),
updatedAt: new Date(),
};
this.sagaStates.set(event.orderId, sagaState);
// 更新订单状态为 PENDING
await this.updateOrderStatus(event.orderId, OrderStatus.PENDING);
// 依次执行各个步骤
await this.executeSteps(event.orderId);
}
/**
* 处理订单取消事件
* 触发补偿流程
*/
async handleOrderCancelled(event: OrderCreatedEvent): Promise<void> {
const sagaState = this.sagaStates.get(event.orderId);
if (!sagaState) {
console.log(`[OrderSaga] No active saga for order ${event.orderId}`);
return;
}
// 启动补偿流程
await this.executeCompensation(event.orderId, 'USER_CANCELLED');
}
/**
* 依次执行 Saga 步骤
*/
private async executeSteps(orderId: string): Promise<void> {
const sagaState = this.sagaStates.get(orderId);
if (!sagaState) return;
for (let i = sagaState.currentStep; i < STEP_EXECUTION_ORDER.length; i++) {
const stepName = STEP_EXECUTION_ORDER[i];
console.log(`[OrderSaga] Executing step ${stepName} for order ${sagaState.orderNumber}`);
const step = this.createStep(stepName, sagaState);
const result = await this.executeStepWithRetry(step);
sagaState.stepResults[stepName] = result;
sagaState.currentStep = i;
if (!result.success) {
console.error(
`[OrderSaga] Step ${stepName} failed for order ${sagaState.orderNumber}: ${result.error}`,
);
// 触发补偿流程
await this.executeCompensation(orderId, result.error || 'STEP_FAILED');
return;
}
// 步骤成功,记录完成
sagaState.completedSteps.push(stepName);
// 更新订单状态
const newStatus = STEP_TO_STATUS_MAP[stepName];
if (newStatus) {
await this.updateOrderStatus(orderId, newStatus, {
stepName,
stepData: result.data,
});
}
sagaState.updatedAt = new Date();
}
// 所有步骤执行完成
console.log(`[OrderSaga] All steps completed for order ${sagaState.orderNumber}`);
// 发布订单完成事件
const orderCompletedEvent = new OrderCompletedEvent(
sagaState.orderId,
sagaState.orderNumber,
);
this.eventBus.publish(orderCompletedEvent);
// 清理 Saga 状态
this.sagaStates.delete(orderId);
}
/**
* 创建指定步骤的处理器
*/
private createStep(stepName: string, sagaState: SagaState): ISagaStep {
switch (stepName) {
case SAGA_STEPS.RESERVE_INVENTORY:
return this.createReserveInventoryStep(sagaState);
case SAGA_STEPS.PROCESS_PAYMENT:
return this.createProcessPaymentStep(sagaState);
case SAGA_STEPS.CREATE_SHIPMENT:
return this.createCreateShipmentStep(sagaState);
default:
throw new Error(`Unknown step: ${stepName}`);
}
}
/**
* 创建库存预占步骤
*/
private createReserveInventoryStep(sagaState: SagaState): ISagaStep {
return {
name: SAGA_STEPS.RESERVE_INVENTORY,
execute: async () => {
// 发布库存预占命令到 Redis Stream
// 等待库存服务响应(这里简化为直接调用)
// 实际实现中应该等待消息回调
const reservationId = `RES-${Date.now()}`;
// 发布事件到库存服务
await this.streamPublisher.publishInventoryEvent(
{
orderId: sagaState.orderId,
orderNumber: sagaState.orderNumber,
reservationId,
action: 'RESERVE',
},
'InventoryReservationRequested',
);
// 模拟库存预占成功
// 实际实现中,这里应该等待来自库存服务的响应事件
return {
success: true,
data: {
reservationId,
reservedItems: [],
},
};
},
compensate: async () => {
await this.inventoryCompensation.execute(sagaState.orderId);
},
getTimeout: () => this.STEP_TIMEOUT,
};
}
/**
* 创建支付处理步骤
*/
private createProcessPaymentStep(sagaState: SagaState): ISagaStep {
return {
name: SAGA_STEPS.PROCESS_PAYMENT,
execute: async () => {
const transactionId = `TXN-${Date.now()}`;
// 发布支付命令
await this.streamPublisher.publishPaymentEvent(
{
orderId: sagaState.orderId,
orderNumber: sagaState.orderNumber,
transactionId,
action: 'CHARGE',
},
'PaymentChargeRequested',
);
// 模拟支付成功
return {
success: true,
data: {
transactionId,
chargedAt: new Date(),
},
};
},
compensate: async () => {
const paymentResult = sagaState.stepResults[SAGA_STEPS.PROCESS_PAYMENT];
if (paymentResult?.data?.transactionId) {
await this.paymentCompensation.execute(
sagaState.orderId,
paymentResult.data.transactionId,
);
}
},
getTimeout: () => this.STEP_TIMEOUT,
};
}
/**
* 创建物流下单步骤
*/
private createCreateShipmentStep(sagaState: SagaState): ISagaStep {
return {
name: SAGA_STEPS.CREATE_SHIPMENT,
execute: async () => {
const shipmentId = `SHP-${Date.now()}`;
const trackingNumber = `TRK${Date.now()}`;
// 发布物流下单命令
await this.streamPublisher.publishLogisticsEvent(
{
orderId: sagaState.orderId,
orderNumber: sagaState.orderNumber,
shipmentId,
action: 'CREATE',
},
'ShipmentCreationRequested',
);
// 模拟物流下单成功
return {
success: true,
data: {
shipmentId,
trackingNumber,
carrier: 'SF Express',
estimatedDelivery: new Date(Date.now() + 3 * 24 * 60 * 60 * 1000),
},
};
},
compensate: async () => {
const shipmentResult = sagaState.stepResults[SAGA_STEPS.CREATE_SHIPMENT];
if (shipmentResult?.data?.shipmentId) {
await this.logisticsCompensation.execute(
sagaState.orderId,
shipmentResult.data.shipmentId,
);
}
},
getTimeout: () => this.STEP_TIMEOUT,
};
}
/**
* 带重试的步骤执行
*/
private async executeStepWithRetry(step: ISagaStep): Promise<SagaStepResult> {
let lastError: string | undefined;
for (let attempt = 1; attempt <= this.MAX_RETRIES; attempt++) {
try {
console.log(`[OrderSaga] Executing step ${step.name}, attempt ${attempt}`);
// 使用 Promise.race 实现超时控制
const result = await Promise.race([
step.execute(),
this.createTimeout(step.getTimeout ? step.getTimeout() : this.STEP_TIMEOUT),
]);
if (result.success) {
return result;
}
lastError = result.error;
} catch (error: any) {
lastError = error.message;
console.error(
`[OrderSaga] Step ${step.name} failed on attempt ${attempt}:`,
error,
);
}
// 重试前等待
if (attempt < this.MAX_RETRIES) {
await this.delay(Math.pow(2, attempt) * 100); // 指数退避
}
}
return {
success: false,
error: lastError || 'Max retries exceeded',
errorCode: 'STEP_EXECUTION_FAILED',
};
}
/**
* 执行补偿流程
*/
private async executeCompensation(
orderId: string,
reason: string,
): Promise<void> {
const sagaState = this.sagaStates.get(orderId);
if (!sagaState) return;
console.log(
`[OrderSaga] Starting compensation for order ${sagaState.orderNumber}, reason: ${reason}`,
);
sagaState.compensationInProgress = true;
sagaState.updatedAt = new Date();
// 发布补偿开始事件
const compensationStartedEvent = new SagaCompensationStartedEvent(
sagaState.orderId,
sagaState.orderNumber,
STEP_EXECUTION_ORDER[sagaState.currentStep],
reason,
);
this.eventBus.publish(compensationStartedEvent);
// 按相反顺序执行已成功步骤的补偿
const completedSteps = [...sagaState.completedSteps].reverse();
for (const stepName of completedSteps) {
try {
console.log(`[OrderSaga] Compensating step ${stepName}`);
const step = this.createStep(stepName, sagaState);
await step.compensate();
console.log(`[OrderSaga] Step ${stepName} compensated successfully`);
} catch (error) {
console.error(
`[OrderSaga] Compensation failed for step ${stepName}:`,
error,
);
// 补偿失败的处理:记录日志,继续执行其他补偿
// 后续可以通过定时任务或死信队列处理补偿失败的情况
}
}
// 更新订单状态为取消
await this.updateOrderStatus(orderId, OrderStatus.CANCELLED, {
compensationReason: reason,
});
// 发布补偿完成事件
const compensationCompletedEvent = new SagaCompensationStartedEvent(
sagaState.orderId,
sagaState.orderNumber,
STEP_EXECUTION_ORDER[sagaState.currentStep],
reason,
);
this.eventBus.publish(compensationCompletedEvent);
// 清理 Saga 状态
this.sagaStates.delete(orderId);
}
/**
* 更新订单状态
*/
private async updateOrderStatus(
orderId: string,
status: OrderStatus,
metadata?: Record<string, any>,
): Promise<void> {
const order = await this.orderRepository.findOne({ where: { id: orderId } });
if (order) {
order.transitionTo(status, metadata);
await this.orderRepository.save(order);
}
}
/**
* 创建超时 Promise
*/
private createTimeout(ms: number): Promise<never> {
return new Promise((_, reject) => {
setTimeout(() => reject(new Error('Step execution timed out')), ms);
});
}
/**
* 延迟函数
*/
private delay(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
/**
* 获取当前 Saga 状态(用于监控和调试)
*/
getSagaState(orderId: string): SagaState | undefined {
return this.sagaStates.get(orderId);
}
/**
* 获取所有活跃的 Saga(用于监控)
*/
getAllActiveSagas(): SagaState[] {
return Array.from(this.sagaStates.values());
}
}
6.3 补偿机制实现
补偿是 Saga 模式确保最终一致性的关键。每个业务步骤都有对应的补偿操作,当后续步骤失败时,依次执行已成功步骤的补偿操作。
// src/saga/compensations/inventory.compensation.ts
import { Injectable, Inject } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Order, OrderStatus } from '../../common/entities/order.entity';
import { OrderItem } from '../../common/entities/order-item.entity';
import { StreamPublisherService } from '../../events/stream-publisher.service';
import { InventoryReleasedEvent } from '../../events/order.events';
/**
* 库存补偿处理器
* 负责释放已预占的库存
*/
@Injectable()
export class InventoryCompensation {
constructor(
@InjectRepository(Order)
private readonly orderRepository: Repository<Order>,
@InjectRepository(OrderItem)
private readonly orderItemRepository: Repository<OrderItem>,
private readonly streamPublisher: StreamPublisherService,
) {}
/**
* 执行库存释放补偿
*/
async execute(orderId: string): Promise<void> {
console.log(`[InventoryCompensation] Releasing inventory for order ${orderId}`);
// 获取订单信息
const order = await this.orderRepository.findOne({
where: { id: orderId },
relations: ['items'],
});
if (!order) {
console.warn(`[InventoryCompensation] Order ${orderId} not found`);
return;
}
// 获取已预占的库存项
const reservedItems = order.items.filter(
(item) => item.inventoryReservationId,
);
if (reservedItems.length === 0) {
console.log(`[InventoryCompensation] No reserved inventory for order ${orderId}`);
return;
}
// 发布库存释放事件
const releaseEvent = new InventoryReleasedEvent(
orderId,
order.orderNumber,
reservedItems[0].inventoryReservationId!,
reservedItems.map((item) => ({
productId: item.productId,
quantity: item.quantity,
})),
);
await this.streamPublisher.publishInventoryEvent(releaseEvent, 'InventoryReleased');
// 更新订单项的预占ID(清除)
for (const item of reservedItems) {
item.inventoryReservationId = undefined;
}
await this.orderItemRepository.save(reservedItems);
// 更新订单状态
order.status = OrderStatus.INVENTORY_FAILED;
await this.orderRepository.save(order);
console.log(
`[InventoryCompensation] Inventory released for order ${orderId}`,
);
}
/**
* 检查补偿是否已完成
*/
async isCompensationComplete(orderId: string): Promise<boolean> {
const order = await this.orderRepository.findOne({ where: { id: orderId } });
return order?.status === OrderStatus.INVENTORY_FAILED ||
order?.status === OrderStatus.CANCELLED;
}
}
// src/saga/compensations/payment.compensation.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Order, OrderStatus } from '../../common/entities/order.entity';
import { StreamPublisherService } from '../../events/stream-publisher.service';
import { RefundCompletedEvent } from '../../events/order.events';
/**
* 支付补偿处理器
* 负责处理支付失败后的退款
*/
@Injectable()
export class PaymentCompensation {
constructor(
@InjectRepository(Order)
private readonly orderRepository: Repository<Order>,
private readonly streamPublisher: StreamPublisherService,
) {}
/**
* 执行支付退款补偿
*/
async execute(orderId: string, transactionId: string): Promise<void> {
console.log(
`[PaymentCompensation] Processing refund for order ${orderId}, transaction ${transactionId}`,
);
// 获取订单信息
const order = await this.orderRepository.findOne({ where: { id: orderId } });
if (!order) {
console.warn(`[PaymentCompensation] Order ${orderId} not found`);
return;
}
// 发布退款请求事件
// 实际实现中,支付服务会监听此事件并执行退款
await this.streamPublisher.publishPaymentEvent(
{
orderId,
orderNumber: order.orderNumber,
transactionId,
refundAmount: order.paymentAmount,
action: 'REFUND',
},
'PaymentRefundRequested',
);
// 生成退款ID(模拟)
const refundId = `REF-${Date.now()}`;
// 发布退款完成事件
const refundCompletedEvent = new RefundCompletedEvent(
orderId,
order.orderNumber,
refundId,
Number(order.paymentAmount),
);
await this.streamPublisher.publishPaymentEvent(
refundCompletedEvent,
'RefundCompleted',
);
// 更新订单状态
order.status = OrderStatus.REFUND_COMPLETED;
order.paymentTransactionId = undefined;
await this.orderRepository.save(order);
console.log(
`[PaymentCompensation] Refund processed for order ${orderId}, refundId: ${refundId}`,
);
}
/**
* 检查退款是否已完成
*/
async isCompensationComplete(orderId: string): Promise<boolean> {
const order = await this.orderRepository.findOne({ where: { id: orderId } });
return order?.status === OrderStatus.REFUND_COMPLETED ||
order?.status === OrderStatus.CANCELLED;
}
}
// src/saga/compensations/logistics.compensation.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Order, OrderStatus } from '../../common/entities/order.entity';
import { StreamPublisherService } from '../../events/stream-publisher.service';
import { ShipmentCancelledEvent } from '../../events/order.events';
/**
* 物流补偿处理器
* 负责取消已创建的物流订单
*/
@Injectable()
export class LogisticsCompensation {
constructor(
@InjectRepository(Order)
private readonly orderRepository: Repository<Order>,
private readonly streamPublisher: StreamPublisherService,
) {}
/**
* 执行物流取消补偿
*/
async execute(orderId: string, shipmentId: string): Promise<void> {
console.log(
`[LogisticsCompensation] Cancelling shipment ${shipmentId} for order ${orderId}`,
);
// 获取订单信息
const order = await this.orderRepository.findOne({ where: { id: orderId } });
if (!order) {
console.warn(`[LogisticsCompensation] Order ${orderId} not found`);
return;
}
// 发布物流取消事件
const cancelEvent = new ShipmentCancelledEvent(
orderId,
order.orderNumber,
shipmentId,
);
await this.streamPublisher.publishLogisticsEvent(cancelEvent, 'ShipmentCancelled');
// 更新订单状态
order.status = OrderStatus.SHIPPING_FAILED;
order.trackingNumber = undefined;
await this.orderRepository.save(order);
console.log(
`[LogisticsCompensation] Shipment ${shipmentId} cancelled for order ${orderId}`,
);
}
/**
* 检查物流取消是否已完成
*/
async isCompensationComplete(orderId: string): Promise<boolean> {
const order = await this.orderRepository.findOne({ where: { id: orderId } });
return order?.status === OrderStatus.SHIPPING_FAILED ||
order?.status === OrderStatus.CANCELLED;
}
}
七、外部服务集成
7.1 库存服务集成
库存服务负责管理商品库存,支持库存预占和释放操作。在电商系统中,库存服务的准确性和性能直接影响订单处理能力。
// src/integrations/inventory/inventory.service.ts
import { Injectable } from '@nestjs/common';
import { EventBus } from '@nestjs/cqrs';
import { InventoryReservedEvent, InventoryReservationFailedEvent } from '../../events/order.events';
/**
* 库存服务接口
* 定义库存操作的标准方法
*/
export interface IInventoryReservation {
reservationId: string;
productId: string;
quantity: number;
reservedAt: Date;
expiresAt: Date;
}
export interface IInventoryRelease {
reservationId: string;
releasedAt: Date;
}
/**
* 库存服务
* 模拟实现,实际项目中应调用真实的库存微服务
*/
@Injectable()
export class InventoryService {
// 内存中的库存模拟数据
private inventory: Map<string, number> = new Map([
['PROD-001', 100],
['PROD-002', 50],
['PROD-003', 200],
]);
// 预占记录
private reservations: Map<string, IInventoryReservation> = new Map();
constructor(private readonly eventBus: EventBus) {}
/**
* 预占库存
* @param orderId 订单ID
* @param orderNumber 订单编号
* @param items 预占的商品项
*/
async reserveInventory(
orderId: string,
orderNumber: string,
items: Array<{ productId: string; quantity: number }>,
): Promise<{
success: boolean;
reservationId?: string;
failedItems?: Array<{
productId: string;
requestedQuantity: number;
availableQuantity: number;
}>;
}> {
console.log(
`[InventoryService] Reserving inventory for order ${orderNumber}`,
);
const reservationId = `RES-${Date.now()}-${Math.random().toString(36).substr(2, 6)}`;
const failedItems: Array<{
productId: string;
requestedQuantity: number;
availableQuantity: number;
}> = [];
// 检查每个商品的库存
for (const item of items) {
const available = this.inventory.get(item.productId) || 0;
if (available < item.quantity) {
failedItems.push({
productId: item.productId,
requestedQuantity: item.quantity,
availableQuantity: available,
});
}
}
// 如果有商品库存不足,返回失败
if (failedItems.length > 0) {
const failedEvent = new InventoryReservationFailedEvent(
orderId,
orderNumber,
'Insufficient inventory',
failedItems,
);
this.eventBus.publish(failedEvent);
return {
success: false,
failedItems,
};
}
// 扣除库存
for (const item of items) {
const currentStock = this.inventory.get(item.productId) || 0;
this.inventory.set(item.productId, currentStock - item.quantity);
// 记录预占
this.reservations.set(`${reservationId}:${item.productId}`, {
reservationId,
productId: item.productId,
quantity: item.quantity,
reservedAt: new Date(),
expiresAt: new Date(Date.now() + 30 * 60 * 1000), // 30分钟过期
});
}
// 发布库存预占成功事件
const reservedEvent = new InventoryReservedEvent(
orderId,
orderNumber,
reservationId,
items.map((item) => ({
productId: item.productId,
quantity: item.quantity,
reservationId,
})),
);
this.eventBus.publish(reservedEvent);
console.log(
`[InventoryService] Inventory reserved successfully, reservationId: ${reservationId}`,
);
return {
success: true,
reservationId,
};
}
/**
* 释放预占的库存
*/
async releaseInventory(
orderId: string,
orderNumber: string,
reservationId: string,
items: Array<{ productId: string; quantity: number }>,
): Promise<void> {
console.log(
`[InventoryService] Releasing inventory for reservation ${reservationId}`,
);
// 归还库存
for (const item of items) {
const currentStock = this.inventory.get(item.productId) || 0;
this.inventory.set(item.productId, currentStock + item.quantity);
// 删除预占记录
this.reservations.delete(`${reservationId}:${item.productId}`);
}
console.log(
`[InventoryService] Inventory released for reservation ${reservationId}`,
);
}
/**
* 查询商品库存
*/
async getInventory(productId: string): Promise<number> {
return this.inventory.get(productId) || 0;
}
/**
* 补充库存(仅用于测试和初始化)
*/
async addInventory(productId: string, quantity: number): Promise<void> {
const currentStock = this.inventory.get(productId) || 0;
this.inventory.set(productId, currentStock + quantity);
}
}
// src/integrations/inventory/inventory.events-handler.ts
import { Injectable } from '@nestjs/common';
import { EventsHandler, IEventHandler } from '@nestjs/cqrs';
import { InventoryReservationRequestedEvent } from './events';
/**
* 库存事件处理器
* 监听来自 Saga 的库存操作请求
*/
@Injectable()
@EventsHandler(InventoryReservationRequestedEvent)
export class InventoryEventHandler
implements IEventHandler<InventoryReservationRequestedEvent>
{
constructor(private readonly inventoryService: InventoryService) {}
async handle(event: InventoryReservationRequestedEvent): Promise<void> {
console.log(
`[InventoryEventHandler] Received reservation request for order ${event.orderNumber}`,
);
if (event.action === 'RESERVE') {
await this.inventoryService.reserveInventory(
event.orderId,
event.orderNumber,
event.items,
);
} else if (event.action === 'RELEASE') {
await this.inventoryService.releaseInventory(
event.orderId,
event.orderNumber,
event.reservationId,
event.items,
);
}
}
}
7.2 支付服务集成
支付服务负责处理订单支付,包括扣款和退款操作。支付是电商系统中最敏感的模块,涉及资金安全,需要特别关注幂等性和安全性。
// src/integrations/payment/payment.service.ts
import { Injectable } from '@nestjs/common';
import { EventBus } from '@nestjs/cqrs';
import {
PaymentInitiatedEvent,
PaymentCompletedEvent,
PaymentFailedEvent,
RefundCompletedEvent,
} from '../../events/order.events';
import { PaymentMethod } from '../../common/entities/order.entity';
/**
* 支付结果
*/
export interface PaymentResult {
success: boolean;
transactionId?: string;
errorCode?: string;
errorMessage?: string;
}
/**
* 退款结果
*/
export interface RefundResult {
success: boolean;
refundId?: string;
errorCode?: string;
errorMessage?: string;
}
/**
* 支付服务
* 模拟实现,实际项目中应调用真实的支付网关
*/
@Injectable()
export class PaymentService {
// 交易记录
private transactions: Map<string, {
transactionId: string;
orderId: string;
amount: number;
status: 'PENDING' | 'COMPLETED' | 'FAILED' | 'REFUNDED';
createdAt: Date;
}> = new Map();
// 退款记录
private refunds: Map<string, {
refundId: string;
transactionId: string;
amount: number;
status: 'PENDING' | 'COMPLETED' | 'FAILED';
createdAt: Date;
}> = new Map();
constructor(private readonly eventBus: EventBus) {}
/**
* 处理支付
*/
async processPayment(
orderId: string,
orderNumber: string,
userId: string,
amount: number,
paymentMethod: PaymentMethod,
): Promise<PaymentResult> {
console.log(
`[PaymentService] Processing payment for order ${orderNumber}, amount: ${amount}`,
);
// 生成交易ID
const transactionId = `TXN-${Date.now()}-${Math.random().toString(36).substr(2, 8)}`;
// 发布支付发起事件
const initiatedEvent = new PaymentInitiatedEvent(
orderId,
orderNumber,
userId,
amount,
paymentMethod,
);
this.eventBus.publish(initiatedEvent);
// 模拟支付网关调用
try {
// 模拟支付成功(实际应根据支付网关返回)
const success = await this.simulatePaymentGateway(amount);
if (success) {
// 记录交易
this.transactions.set(transactionId, {
transactionId,
orderId,
amount,
status: 'COMPLETED',
createdAt: new Date(),
});
// 发布支付成功事件
const completedEvent = new PaymentCompletedEvent(
orderId,
orderNumber,
transactionId,
amount,
);
this.eventBus.publish(completedEvent);
console.log(
`[PaymentService] Payment completed for order ${orderNumber}, transactionId: ${transactionId}`,
);
return {
success: true,
transactionId,
};
} else {
// 发布支付失败事件
const failedEvent = new PaymentFailedEvent(
orderId,
orderNumber,
'Payment declined by gateway',
'DECLINED',
);
this.eventBus.publish(failedEvent);
return {
success: false,
errorCode: 'DECLINED',
errorMessage: 'Payment was declined',
};
}
} catch (error: any) {
const failedEvent = new PaymentFailedEvent(
orderId,
orderNumber,
error.message,
'GATEWAY_ERROR',
);
this.eventBus.publish(failedEvent);
return {
success: false,
errorCode: 'GATEWAY_ERROR',
errorMessage: error.message,
};
}
}
/**
* 处理退款
*/
async processRefund(
orderId: string,
orderNumber: string,
transactionId: string,
refundAmount: number,
): Promise<RefundResult> {
console.log(
`[PaymentService] Processing refund for order ${orderNumber}, amount: ${refundAmount}`,
);
// 查找原交易
const transaction = this.transactions.get(transactionId);
if (!transaction) {
return {
success: false,
errorCode: 'TRANSACTION_NOT_FOUND',
errorMessage: 'Original transaction not found',
};
}
if (transaction.status === 'REFUNDED') {
return {
success: false,
errorCode: 'ALREADY_REFUNDED',
errorMessage: 'Transaction already refunded',
};
}
// 生成退款ID
const refundId = `REF-${Date.now()}-${Math.random().toString(36).substr(2, 8)}`;
try {
// 模拟退款网关调用
const success = await this.simulateRefundGateway(refundAmount);
if (success) {
// 更新交易状态
transaction.status = 'REFUNDED';
// 记录退款
this.refunds.set(refundId, {
refundId,
transactionId,
amount: refundAmount,
status: 'COMPLETED',
createdAt: new Date(),
});
// 发布退款完成事件
const completedEvent = new RefundCompletedEvent(
orderId,
orderNumber,
refundId,
refundAmount,
);
this.eventBus.publish(completedEvent);
console.log(
`[PaymentService] Refund completed for order ${orderNumber}, refundId: ${refundId}`,
);
return {
success: true,
refundId,
};
} else {
return {
success: false,
errorCode: 'REFUND_FAILED',
errorMessage: 'Refund processing failed',
};
}
} catch (error: any) {
return {
success: false,
errorCode: 'GATEWAY_ERROR',
errorMessage: error.message,
};
}
}
/**
* 获取交易状态
*/
getTransactionStatus(transactionId: string): string | undefined {
return this.transactions.get(transactionId)?.status;
}
/**
* 模拟支付网关调用
*/
private async simulatePaymentGateway(amount: number): Promise<boolean> {
// 模拟网络延迟
await new Promise((resolve) => setTimeout(resolve, 100));
// 模拟 95% 成功率
return Math.random() < 0.95;
}
/**
* 模拟退款网关调用
*/
private async simulateRefundGateway(amount: number): Promise<boolean> {
// 模拟网络延迟
await new Promise((resolve) => setTimeout(resolve, 100));
// 模拟 98% 成功率
return Math.random() < 0.98;
}
}
7.3 物流服务集成
物流服务负责创建运单和物流追踪。在订单系统中,物流服务通常由第三方物流提供商提供接口对接。
// src/integrations/logistics/logistics.service.ts
import { Injectable } from '@nestjs/common';
import { EventBus } from '@nestjs/cqrs';
import {
ShipmentCreatedEvent,
ShipmentCreationFailedEvent,
ShipmentCancelledEvent,
} from '../../events/order.events';
import { ShippingMethod } from '../../common/entities/order.entity';
/**
* 物流服务
* 模拟实现,实际项目中应调用真实的物流服务API
*/
@Injectable()
export class LogisticsService {
// 运单记录
private shipments: Map<string, {
shipmentId: string;
orderId: string;
trackingNumber: string;
carrier: string;
status: 'CREATED' | 'PICKED_UP' | 'IN_TRANSIT' | 'DELIVERED' | 'CANCELLED';
estimatedDelivery: Date;
createdAt: Date;
}> = new Map();
constructor(private readonly eventBus: EventBus) {}
/**
* 创建运单
*/
async createShipment(
orderId: string,
orderNumber: string,
shippingAddress: string,
shippingMethod: ShippingMethod,
items: Array<{ productName: string; quantity: number }>,
): Promise<{
success: boolean;
shipmentId?: string;
trackingNumber?: string;
carrier?: string;
estimatedDelivery?: Date;
errorMessage?: string;
}> {
console.log(
`[LogisticsService] Creating shipment for order ${orderNumber}`,
);
// 生成运单ID和追踪号
const shipmentId = `SHP-${Date.now()}-${Math.random().toString(36).substr(2, 6)}`;
const trackingNumber = this.generateTrackingNumber();
const carrier = this.selectCarrier(shippingMethod);
try {
// 模拟调用物流API
const success = await this.simulateLogisticsApiCall();
if (success) {
// 计算预计送达时间
const estimatedDelivery = this.calculateEstimatedDelivery(shippingMethod);
// 记录运单
this.shipments.set(shipmentId, {
shipmentId,
orderId,
trackingNumber,
carrier,
status: 'CREATED',
estimatedDelivery,
createdAt: new Date(),
});
// 发布运单创建成功事件
const createdEvent = new ShipmentCreatedEvent(
orderId,
orderNumber,
shipmentId,
trackingNumber,
carrier,
estimatedDelivery,
);
this.eventBus.publish(createdEvent);
console.log(
`[LogisticsService] Shipment created for order ${orderNumber}, trackingNumber: ${trackingNumber}`,
);
return {
success: true,
shipmentId,
trackingNumber,
carrier,
estimatedDelivery,
};
} else {
throw new Error('Logistics API returned failure');
}
} catch (error: any) {
// 发布运单创建失败事件
const failedEvent = new ShipmentCreationFailedEvent(
orderId,
orderNumber,
error.message,
);
this.eventBus.publish(failedEvent);
return {
success: false,
errorMessage: error.message,
};
}
}
/**
* 取消运单
*/
async cancelShipment(
orderId: string,
orderNumber: string,
shipmentId: string,
): Promise<boolean> {
console.log(
`[LogisticsService] Cancelling shipment ${shipmentId} for order ${orderNumber}`,
);
const shipment = this.shipments.get(shipmentId);
if (!shipment) {
console.warn(
`[LogisticsService] Shipment ${shipmentId} not found`,
);
return false;
}
if (shipment.status === 'IN_TRANSIT' || shipment.status === 'DELIVERED') {
console.warn(
`[LogisticsService] Cannot cancel shipment in status ${shipment.status}`,
);
return false;
}
// 模拟取消API调用
const success = await this.simulateLogisticsApiCall();
if (success) {
shipment.status = 'CANCELLED';
// 发布运单取消成功事件
const cancelledEvent = new ShipmentCancelledEvent(
orderId,
orderNumber,
shipmentId,
);
this.eventBus.publish(cancelledEvent);
console.log(
`[LogisticsService] Shipment ${shipmentId} cancelled`,
);
return true;
}
return false;
}
/**
* 获取运单状态
*/
async getShipmentStatus(shipmentId: string): Promise<string | undefined> {
return this.shipments.get(shipmentId)?.status;
}
/**
* 生成追踪号
*/
private generateTrackingNumber(): string {
const prefix = 'SF';
const timestamp = Date.now().toString(36).toUpperCase();
const random = Math.random().toString(36).substr(2, 6).toUpperCase();
return `${prefix}${timestamp}${random}`;
}
/**
* 选择物流承运商
*/
private selectCarrier(shippingMethod: ShippingMethod): string {
switch (shippingMethod) {
case ShippingMethod.EXPRESS:
return 'SF Express';
case ShippingMethod.STANDARD:
return 'YTO Express';
case ShippingMethod.PICKUP:
return 'Self Pickup';
default:
return 'Standard Carrier';
}
}
/**
* 计算预计送达时间
*/
private calculateEstimatedDelivery(shippingMethod: ShippingMethod): Date {
const now = new Date();
let daysToAdd = 7; // 默认7天
switch (shippingMethod) {
case ShippingMethod.EXPRESS:
daysToAdd = 2;
break;
case ShippingMethod.STANDARD:
daysToAdd = 5;
break;
case ShippingMethod.PICKUP:
daysToAdd = 1;
break;
}
return new Date(now.getTime() + daysToAdd * 24 * 60 * 60 * 1000);
}
/**
* 模拟物流API调用
*/
private async simulateLogisticsApiCall(): Promise<boolean> {
// 模拟网络延迟
await new Promise((resolve) => setTimeout(resolve, 150));
// 模拟 90% 成功率
return Math.random() < 0.9;
}
}
八、NestJS 模块组装与控制器
8.1 应用模块组装
最后,我们需要将所有模块组装到 NestJS 应用中,并创建控制器来处理 HTTP 请求。
// src/app.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { CqrsModule } from '@nestjs/cqrs';
import { RedisModule } from './config/redis.config';
import { Order } from './common/entities/order.entity';
import { OrderItem } from './common/entities/order-item.entity';
import { CreateOrderHandler } from './commands/handlers/create-order.handler';
import { CancelOrderHandler } from './commands/handlers/cancel-order.handler';
import { GetOrderHandler } from './queries/handlers/get-order.handler';
import { GetOrdersHandler } from './queries/handlers/get-orders.handler';
import { OrderSaga } from './saga/order.saga';
import { InventoryCompensation } from './saga/compensations/inventory.compensation';
import { PaymentCompensation } from './saga/compensations/payment.compensation';
import { LogisticsCompensation } from './saga/compensations/logistics.compensation';
import { InventoryService } from './integrations/inventory/inventory.service';
import { PaymentService } from './integrations/payment/payment.service';
import { LogisticsService } from './integrations/logistics/logistics.service';
import { StreamPublisherService } from './events/stream-publisher.service';
import { StreamConsumerService } from './events/stream-consumer.service';
import { OrderController } from './order.controller';
import { Redis } from 'ioredis';
// 数据库配置
const DATABASE_CONFIG = {
type: 'postgres' as const,
host: process.env.DB_HOST || 'localhost',
port: parseInt(process.env.DB_PORT || '5432', 10),
username: process.env.DB_USERNAME || 'postgres',
password: process.env.DB_PASSWORD || 'postgres',
database: process.env.DB_NAME || 'ecommerce',
entities: [Order, OrderItem],
synchronize: true, // 开发环境设为true,生产环境应使用 migrations
logging: process.env.NODE_ENV === 'development',
};
// 命令处理器
const COMMAND_HANDLERS = [CreateOrderHandler, CancelOrderHandler];
// 查询处理器
const QUERY_HANDLERS = [GetOrderHandler, GetOrdersHandler];
@Module({
imports: [
// TypeORM 配置
TypeOrmModule.forRoot(DATABASE_CONFIG),
TypeOrmModule.forFeature([Order, OrderItem]),
// CQRS 模块
CqrmModule,
// Redis 模块
RedisModule,
],
controllers: [OrderController],
providers: [
// 事件服务
StreamPublisherService,
StreamConsumerService,
// Saga 组件
OrderSaga,
InventoryCompensation,
PaymentCompensation,
LogisticsCompensation,
// 外部服务
InventoryService,
PaymentService,
LogisticsService,
// 命令处理器
...COMMAND_HANDLERS,
// 查询处理器
...QUERY_HANDLERS,
],
exports: [
StreamPublisherService,
StreamConsumerService,
OrderSaga,
InventoryService,
PaymentService,
LogisticsService,
],
})
export class AppModule {}
8.2 订单控制器
控制器负责处理 HTTP 请求,将客户端的请求转发给 CQRS 的命令处理器或查询处理器。
// src/order.controller.ts
import {
Controller,
Post,
Get,
Delete,
Body,
Param,
Query,
HttpCode,
HttpStatus,
UsePipes,
ValidationPipe,
} from '@nestjs/common';
import { CommandBus, QueryBus } from '@nestjs/cqrs';
import { CreateOrderCommand } from './commands/commands/create-order.command';
import { CancelOrderCommand } from './commands/commands/cancel-order.command';
import { GetOrderQuery } from './queries/queries/get-order.query';
import { GetOrdersQuery } from './queries/queries/get-orders.query';
import {
CreateOrderDto,
ShippingAddressDto,
} from './common/dto/create-order.dto';
import {
OrderResponseDto,
OrderListResponseDto,
} from './common/dto/order-response.dto';
/**
* 订单控制器
* 处理所有订单相关的 HTTP 请求
*/
@Controller('api/orders')
@UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
export class OrderController {
constructor(
private readonly commandBus: CommandBus,
private readonly queryBus: QueryBus,
) {}
/**
* 创建订单
* POST /api/orders
*/
@Post()
@HttpCode(HttpStatus.CREATED)
async createOrder(@Body() createOrderDto: CreateOrderDto): Promise<OrderResponseDto> {
const command = new CreateOrderCommand(
createOrderDto.userId,
createOrderDto.items,
createOrderDto.shippingAddress,
createOrderDto.paymentMethod,
createOrderDto.shippingMethod,
createOrderDto.remark,
createOrderDto.couponCodes,
);
const order = await this.commandBus.execute(command);
return this.mapToResponse(order);
}
/**
* 获取订单详情
* GET /api/orders/:id
*/
@Get(':id')
async getOrder(
@Param('id') orderId: string,
@Query('userId') userId?: string,
): Promise<OrderResponseDto | null> {
const query = new GetOrderQuery(orderId, userId);
const order = await this.queryBus.execute(query);
return order ? this.mapToResponse(order) : null;
}
/**
* 获取订单列表
* GET /api/orders?userId=xxx&page=1&pageSize=10
*/
@Get()
async getOrders(
@Query('userId') userId: string,
@Query('status') status?: string,
@Query('page') page?: string,
@Query('pageSize') pageSize?: string,
): Promise<OrderListResponseDto> {
const query = new GetOrdersQuery(userId, {
status: status as any,
page: page ? parseInt(page, 10) : 1,
pageSize: pageSize ? parseInt(pageSize, 10) : 10,
});
return await this.queryBus.execute(query);
}
/**
* 取消订单
* DELETE /api/orders/:id
*/
@Delete(':id')
@HttpCode(HttpStatus.OK)
async cancelOrder(
@Param('id') orderId: string,
@Query('userId') userId: string,
@Query('reason') reason: string,
): Promise<OrderResponseDto> {
const command = new CancelOrderCommand(orderId, userId, reason || 'User cancelled');
const order = await this.commandBus.execute(command);
return this.mapToResponse(order);
}
/**
* 映射订单实体到响应 DTO
*/
private mapToResponse(order: any): OrderResponseDto {
return {
id: order.id,
orderNumber: order.orderNumber,
userId: order.userId,
status: order.status,
statusText: this.getStatusText(order.status),
items: order.items?.map((item: any) => ({
productId: item.productId,
productName: item.productName,
productSku: item.productSku,
quantity: item.quantity,
unitPrice: Number(item.unitPrice),
subtotal: Number(item.subtotal),
})) || [],
amount: {
subtotal: Number(order.totalAmount),
freight: Number(order.freightAmount),
discount: Number(order.discountAmount),
payment: Number(order.paymentAmount),
},
payment: {
method: order.paymentMethod,
transactionId: order.paymentTransactionId,
},
shipping: {
method: order.shippingMethod,
trackingNumber: order.trackingNumber,
address: order.shippingAddress ? JSON.parse(order.shippingAddress) : null,
},
timestamps: {
created: order.createdAt,
updated: order.updatedAt,
paid: order.paidAt,
shipped: order.shippedAt,
delivered: order.deliveredAt,
},
};
}
/**
* 获取订单状态的中文描述
*/
private getStatusText(status: string): string {
const statusMap: Record<string, string> = {
PENDING: '待处理',
INVENTORY_RESERVED: '库存已预留',
INVENTORY_FAILED: '库存预占失败',
PAYMENT_PENDING: '等待支付',
PAYMENT_COMPLETED: '支付成功',
PAYMENT_FAILED: '支付失败',
SHIPPING_PENDING: '等待发货',
SHIPPING_PREPARED: '备货中',
SHIPPED: '已发货',
SHIPPING_FAILED: '发货失败',
DELIVERED: '已签收',
CANCELLED: '已取消',
REFUND_PENDING: '退款中',
REFUND_COMPLETED: '退款完成',
};
return statusMap[status] || status;
}
}
8.3 DTO 定义
// src/common/dto/create-order.dto.ts
import {
IsString,
IsUUID,
IsEnum,
IsArray,
ValidateNested,
IsOptional,
Min,
IsPhoneNumber,
} from 'class-validator';
import { Type } from 'class-transformer';
import { PaymentMethod, ShippingMethod } from '../entities/order.entity';
/**
* 收货地址 DTO
*/
export class ShippingAddressDto {
@IsString()
receiverName: string;
@IsString()
receiverPhone: string;
@IsString()
province: string;
@IsString()
city: string;
@IsString()
district: string;
@IsString()
detailAddress: string;
}
/**
* 订单商品项 DTO
*/
export class OrderItemDto {
@IsUUID()
productId: string;
@IsString()
productName: string;
@IsString()
productSku: string;
@Min(1)
quantity: number;
@Min(0)
unitPrice: number;
}
/**
* 创建订单 DTO
*/
export class CreateOrderDto {
@IsUUID()
userId: string;
@IsArray()
@ValidateNested({ each: true })
@Type(() => OrderItemDto)
items: OrderItemDto[];
@ValidateNested()
@Type(() => ShippingAddressDto)
shippingAddress: ShippingAddressDto;
@IsEnum(PaymentMethod)
paymentMethod: PaymentMethod;
@IsEnum(ShippingMethod)
shippingMethod: ShippingMethod;
@IsOptional()
@IsString()
remark?: string;
@IsOptional()
@IsArray()
@IsString({ each: true })
couponCodes?: string[];
}
// src/common/dto/order-response.dto.ts
import { PaymentMethod, ShippingMethod, OrderStatus } from '../entities/order.entity';
/**
* 订单金额信息
*/
export interface OrderAmountDto {
subtotal: number;
freight: number;
discount: number;
payment: number;
}
/**
* 支付信息
*/
export interface PaymentInfoDto {
method: PaymentMethod;
transactionId?: string;
}
/**
* 物流信息
*/
export interface ShippingInfoDto {
method: ShippingMethod;
trackingNumber?: string;
address?: any;
}
/**
* 时间戳信息
*/
export interface TimestampsDto {
created: Date;
updated: Date;
paid?: Date;
shipped?: Date;
delivered?: Date;
}
/**
* 订单商品项响应
*/
export interface OrderItemResponseDto {
productId: string;
productName: string;
productSku: string;
quantity: number;
unitPrice: number;
subtotal: number;
}
/**
* 订单响应 DTO
*/
export class OrderResponseDto {
id: string;
orderNumber: string;
userId: string;
status: OrderStatus;
statusText: string;
items: OrderItemResponseDto[];
amount: OrderAmountDto;
payment: PaymentInfoDto;
shipping: ShippingInfoDto;
timestamps: TimestampsDto;
remark?: string;
}
/**
* 订单列表项响应
*/
export interface OrderListItemDto {
id: string;
orderNumber: string;
status: OrderStatus;
statusText: string;
itemCount: number;
totalAmount: number;
createdAt: Date;
}
/**
* 订单列表响应 DTO
*/
export class OrderListResponseDto {
orders: OrderListItemDto[];
total: number;
page: number;
pageSize: number;
totalPages: number;
}
8.4 应用入口
// src/main.ts
import { NestFactory } from '@nestjs/core';
import { ValidationPipe, Logger } from '@nestjs/common';
import { AppModule } from './app.module';
async function bootstrap() {
const logger = new Logger('Bootstrap');
const app = await NestFactory.create(AppModule, {
logger: ['error', 'warn', 'log', 'debug', 'verbose'],
});
// 全局验证管道
app.useGlobalPipes(
new ValidationPipe({
transform: true,
whitelist: true,
forbidNonWhitelisted: true,
}),
);
// CORS 配置
app.enableCors({
origin: true,
credentials: true,
});
// API 前缀
app.setGlobalPrefix('api/v1');
const port = process.env.PORT || 3000;
await app.listen(port);
logger.log(`Application is running on: http://localhost:${port}/api/v1`);
logger.log(`Environment: ${process.env.NODE_ENV || 'development'}`);
}
bootstrap().catch((error) => {
console.error('Failed to start application:', error);
process.exit(1);
});
九、架构设计分析
9.1 CQRS 在订单系统中的优势
CQRS 模式为订单系统带来了多方面的优势。首先,读写分离实现了性能的最优化。在订单创建时,系统需要严格校验库存、计算金额、生成订单号,这些操作需要强一致性和完整的事务支持;而订单查询时,用户可能需要查看历史订单、筛选特定状态的订单、或者获取订单的物流进展,这些操作更注重查询的灵活性和响应速度。通过 CQRS,命令端可以使用 PostgreSQL 主库保证写入的强一致性,查询端则可以使用只读副本或 MongoDB 来优化读取性能,甚至引入 Redis 缓存来应对高并发的查询请求。
其次,CQRS 使得领域模型更加清晰。命令端的领域模型可以专注于表达业务不变量和状态流转规则,不需要考虑如何支持各种查询场景;查询端的模型则可以针对不同的查询需求进行定制,比如为管理后台提供一个包含所有字段的完整视图,为移动端提供一个精简的视图以减少数据传输量。
第三,系统扩展性得到提升。当订单查询量增长时,可以独立扩展查询端的资源,而不影响命令端的稳定性;当订单写入量增长时,则可以扩展命令端甚至引入分库分表。这种灵活的扩展能力对于电商系统应对促销活动等流量高峰至关重要。
9.2 Redis Stream 作为事件源的优势
Redis Stream 在订单系统中扮演着事件驱动架构的核心角色,相比其他消息队列方案具有独特的优势。从性能角度来看,Redis Stream 继承了 Redis 的高性能特性,单实例即可支持每秒数万条消息的写入和消费,这使得它能够轻松应对订单高峰期的事件流处理需求。相比之下,Kafka 虽然在吞吐量上更具优势,但其配置和运维复杂度较高,对于中小规模的电商系统可能过于重量级。
从功能角度来看,Redis Stream 提供了丰富的特性:基于消费者组的负载均衡和故障转移确保了事件处理的可靠性;消息持久化和重试机制保障了事件的可靠传递;基于索引的随机访问允许消费者灵活地回溯历史消息或跳过已处理的消息;Stream 的长度限制和自动清理机制使得存储管理变得简单。这些特性组合在一起,为订单系统提供了完整的事件流处理能力。
从运维角度来看,Redis 本身就是许多电商系统的标配组件,用于缓存、Session 存储、分布式锁等场景。在现有基础设施上引入 Redis Stream,可以避免部署额外的消息队列组件,降低系统复杂度和技术债务。同时,Redis 的监控和管理工具相对成熟,便于集成到现有的运维体系中。
9.3 Saga 模式处理分布式事务的原理
在微服务架构中,订单处理涉及库存服务、支付服务、物流服务等多个外部系统,传统的分布式事务方案(如两阶段提交)面临诸多挑战:锁等待时间长、系统可用性降低、单点故障风险等。Saga 模式通过另一种思路解决了这一问题:将一个长事务分解为一系列本地事务,每个本地事务完成后立即释放资源并发布事件,如果后续步骤失败,则通过执行前面步骤的补偿操作来恢复一致性。
Saga 模式的核心价值在于将强一致性转换为最终一致性。在订单系统中,虽然库存预占、支付扣款、物流下单这些操作需要协调完成,但它们本质上是独立的业务操作。通过 Saga 编排,系统可以在每个步骤完成后立即响应,而不是像两阶段提交那样锁定资源直到整个事务完成。这大大提升了系统的并发能力和响应速度,而最终一致性通过补偿机制得到保障。
Saga 编排器是整个模式的核心枢纽。它维护着事务的执行状态,管理各步骤的执行顺序和依赖关系,处理失败时的补偿逻辑,甚至实现超时控制和自动重试。在本实现中,编排器不仅执行正向流程,还会在任何步骤失败时自动触发补偿流程,确保系统状态不会停留在中间状态。同时,通过将 Saga 状态快照保存到数据库,系统在重启后能够恢复未完成的 Saga 实例,实现了故障恢复能力。
9.4 事件驱动架构的协同效应
将 CQRS、Saga 和 Redis Stream 三者结合使用,产生了显著的协同效应。事件流作为系统的中枢神经,连接了命令端和查询端,协调了各个微服务之间的协作,实现了系统各部分的有机结合。
事件驱动的第一个重要优势是系统的可观测性和可追溯性。所有订单状态变更都通过事件记录下来,这些事件构成了完整的审计日志。可以随时回放事件来重现系统状态,调查问题根因,或者构建新的读模型来支持新的查询需求。相比传统的直接数据库更新模式,事件溯源提供了更完整的信息保留。
事件驱动的第二个优势是系统组件之间的松耦合。库存服务、支付服务、物流服务都通过事件与订单系统交互,不需要直接调用对方的服务接口。这意味着每个服务都可以独立开发、测试和部署,服务之间不存在直接的代码依赖。当业务需求变化需要修改某个服务的逻辑时,变更的影响被限制在该服务内部,不会波及到其他服务。
事件驱动的第三个优势是系统的弹性和伸缩性。事件消费者可以独立于事件生产者进行扩展,当订单量增长时,只需要增加消费者的实例数量即可。Redis Stream 的消费者组机制会自动在多个消费者之间分配消息,实现负载均衡。同时,事件处理是异步进行的,用户提交订单后可以立即获得响应,而不需要等待整个处理流程完成,这大大提升了用户体验。
9.5 容错和恢复机制
分布式系统面临着各种可能的故障:网络延迟、服务超时、数据库连接耗尽、消息队列不可用等。为了确保订单系统的可靠性,我们需要设计完善的容错和恢复机制。
消息处理的幂等性是容错的基础。由于网络原因或消费者处理失败,消息可能被重复发送或消费。如果处理逻辑不具备幂等性,就会导致数据重复或状态异常。在本实现中,每个服务在处理消息时都会检查当前状态,只有在符合预期时才执行相应操作。例如,库存释放时会检查预占ID是否存在,支付退款时会检查交易是否已经退过。
重试机制是处理临时性故障的主要手段。Saga 的每个步骤都实现了重试逻辑,使用指数退避策略避免重试风暴。同时,每个步骤都设置了超时时间,防止某个步骤长时间阻塞导致整个 Saga 无法推进。当重试次数超过上限时,系统会放弃当前步骤并触发补偿流程。
Saga 状态持久化确保了故障恢复能力。虽然内存中的 Saga 状态提供了快速访问,但真正的状态快照保存在订单数据库的 sagaState 字段中。当应用重启时,可以从数据库恢复进行中的 Saga 实例,继续执行未完成的流程。这避免了用户下单后系统重启导致订单状态丢失的问题。
死信队列和告警机制用于处理无法自动恢复的故障。当消息处理失败达到最大重试次数后,消息会被转移到专门的死信队列供人工干预。同时,系统会发送告警通知运维人员介入处理。这种设计确保了即使在极端情况下,故障也不会被忽视。